欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于Spring Security前后端分离的权限控制系统问题

程序员文章站 2022-10-29 10:12:33
目录3. 自定义userdetails4. 自定义各种handler5. token处理7. 配置websecurity前后端分离的项目,前端有菜单(menu),后端有api(backendapi),...

前后端分离的项目,前端有菜单(menu),后端有api(backendapi),一个menu对应的页面有n个api接口来支持,本文介绍如何基于spring security前后端分离的权限控制系统问题。

话不多说,入正题。一个简单的权限控制系统需要考虑的问题如下:

  1. 权限如何加载
  2. 权限匹配规则
  3. 登录

1. 引入maven依赖

<?xml version="1.0" encoding="utf-8"?>
  <project xmlns="http://maven.apache.org/pom/4.0.0" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
          xsi:schemalocation="http://maven.apache.org/pom/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelversion>4.0.0</modelversion>
     <parent>
         <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-parent</artifactid>
        <version>2.5.1</version>
        <relativepath/> <!-- lookup parent from repository -->
     </parent>
     <groupid>com.example</groupid>
     <artifactid>demo5</artifactid>
     <version>0.0.1-snapshot</version>
     <name>demo5</name>
 
     <properties>
         <java.version>1.8</java.version>
     </properties>
 
     <dependencies>
         <dependency>
             <groupid>org.springframework.boot</groupid>
             <artifactid>spring-boot-starter-data-jpa</artifactid>
         </dependency>
         <dependency>
             <groupid>org.springframework.boot</groupid>
             <artifactid>spring-boot-starter-data-redis</artifactid>
         </dependency>
         <dependency>
             <groupid>org.springframework.boot</groupid>
             <artifactid>spring-boot-starter-security</artifactid>
         </dependency>
         <dependency>
             <groupid>org.springframework.boot</groupid>
             <artifactid>spring-boot-starter-web</artifactid>
         </dependency>
 
         <dependency>
             <groupid>io.jsonwebtoken</groupid>
             <artifactid>jjwt</artifactid>
             <version>0.9.1</version>
         </dependency>
 
         <dependency>
             <groupid>com.alibaba</groupid>
             <artifactid>fastjson</artifactid>
             <version>1.2.76</version>
         </dependency>
         <dependency>
             <groupid>org.apache.commons</groupid>
             <artifactid>commons-lang3</artifactid>
             <version>3.12.0</version>
         </dependency>
         <dependency>
             <groupid>commons-codec</groupid>
             <artifactid>commons-codec</artifactid>
             <version>1.15</version>
         </dependency>
 
         <dependency>
             <groupid>mysql</groupid>
             <artifactid>mysql-connector-java</artifactid>
             <scope>runtime</scope>
         </dependency>
         <dependency>
             <groupid>org.projectlombok</groupid>
             <artifactid>lombok</artifactid>
             <optional>true</optional>
         </dependency>
     </dependencies>
 
     <build>
         <plugins>
             <plugin>
                 <groupid>org.springframework.boot</groupid>
                 <artifactid>spring-boot-maven-plugin</artifactid>
                 <configuration>
                     <excludes>
                         <exclude>
                             <groupid>org.projectlombok</groupid>
                             <artifactid>lombok</artifactid>
                         </exclude>
                     </excludes>
                 </configuration>
             </plugin>
         </plugins>
     </build>
 
 </project>

application.properties配置

server.port=8080
 server.servlet.context-path=/demo
 
 spring.datasource.driver-class-name=com.mysql.jdbc.driver
 spring.datasource.url=jdbc:mysql://localhost:3306/demo?useunicode=true&characterencoding=utf8
 spring.datasource.username=root
 spring.datasource.password=123456
 
spring.jpa.database=mysql
 spring.jpa.open-in-view=true
 spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
 spring.jpa.show-sql=true
 
 spring.redis.host=192.168.28.31
 spring.redis.port=6379
 spring.redis.password=123456

2. 建表并生成相应的实体类

基于Spring Security前后端分离的权限控制系统问题

sysuser.java

package com.example.demo5.entity;
  
  import lombok.getter;
  import lombok.setter;
 
 import javax.persistence.*;
  import java.io.serializable;
  import java.time.localdate;
  import java.util.set;
 
 /**
  * 用户表
  * @author chengjiansheng
  * @date 2021/6/12
  */
 @setter
 @getter
 @entity
 @table(name = "sys_user")
 public class sysuserentity implements serializable {
 
     @id
     @generatedvalue(strategy = generationtype.auto)
     @column(name = "id")
     private integer id;
 
     @column(name = "username")
     private string username;
 
     @column(name = "password")
     private string password;
 
     @column(name = "mobile")
     private string mobile;
 
     @column(name = "enabled")
     private integer enabled;
 
     @column(name = "create_time")
     private localdate createtime;
 
     @column(name = "update_time")
     private localdate updatetime;
 
     @onetoone
     @joincolumn(name = "dept_id")
     private sysdeptentity dept;
 
     @manytomany
     @jointable(name = "sys_user_role",
             joincolumns = {@joincolumn(name = "user_id", referencedcolumnname = "id")},
             inversejoincolumns = {@joincolumn(name = "role_id", referencedcolumnname = "id")})
     private set<sysroleentity> roles;
 
 }

sysdept.java

部门相当于用户组,这里简化了一下,用户组没有跟角色管理

package com.example.demo5.entity;
  
  import lombok.data;
  
  import javax.persistence.*;
  import java.io.serializable;
  import java.util.set;
  
  /**
  * 部门表
  * @author chengjiansheng
  * @date 2021/6/12
  */
 @data
 @entity
 @table(name = "sys_dept")
 public class sysdeptentity implements serializable {
 
     @id
     @generatedvalue(strategy = generationtype.auto)
     @column(name = "id")
     private integer id;
 
     /**
      * 部门名称
      */
     @column(name = "name")
     private string name;
 
     /**
      * 父级部门id
      */
     @column(name = "pid")
     private integer pid;
 
 //    @manytomany(mappedby = "depts")
 //    private set<sysroleentity> roles;
 }

sysmenu.java

菜单相当于权限

package com.example.demo5.entity;
  
  import lombok.data;
  import lombok.getter;
  import lombok.setter;
  
  import javax.persistence.*;
  import java.io.serializable;
 import java.util.set;
 
 /**
  * 菜单表
  * @author chengjiansheng
  * @date 2021/6/12
  */
 @setter
 @getter
 @entity
 @table(name = "sys_menu")
 public class sysmenuentity implements serializable {
 
     @id
     @generatedvalue(strategy = generationtype.auto)
     @column(name = "id")
     private integer id;
 
     /**
      * 资源编码
      */
     @column(name = "code")
     private string code;
 
     /**
      * 资源名称
      */
     @column(name = "name")
     private string name;
 
     /**
      * 菜单/按钮url
      */
     @column(name = "url")
     private string url;
 
     /**
      * 资源类型(1:菜单,2:按钮)
      */
     @column(name = "type")
     private integer type;
 
     /**
      * 父级菜单id
      */
     @column(name = "pid")
     private integer pid;
 
     /**
      * 排序号
      */
     @column(name = "sort")
     private integer sort;
 
     @manytomany(mappedby = "menus")
     private set<sysroleentity> roles;
 
 }

sysrole.java

package com.example.demo5.entity;
  
  import lombok.data;
  import lombok.getter;
  import lombok.setter;
  
  import javax.persistence.*;
  import java.io.serializable;
  import java.util.set;
 
 /**
  * 角色表
  * @author chengjiansheng
  * @date 2021/6/12
  */
 @setter
 @getter
 @entity
 @table(name = "sys_role")
 public class sysroleentity implements serializable {
 
     @id
     @generatedvalue(strategy = generationtype.auto)
     @column(name = "id")
     private integer id;
 
     /**
      * 角色名称
      */
     @column(name = "name")
     private string name;
 
     @manytomany(mappedby = "roles")
     private set<sysuserentity> users;
 
     @manytomany
     @jointable(name = "sys_role_menu",
             joincolumns = {@joincolumn(name = "role_id", referencedcolumnname = "id")},
             inversejoincolumns = {@joincolumn(name = "menu_id", referencedcolumnname = "id")})
     private set<sysmenuentity> menus;
 
 //    @manytomany
 //    @jointable(name = "sys_dept_role",
 //            joincolumns = {@joincolumn(name = "role_id", referencedcolumnname = "id")},
 //            inversejoincolumns = {@joincolumn(name = "dept_id", referencedcolumnname = "id")})
 //    private set<sysdeptentity> depts;
 
 }

注意,不要使用@data注解,因为@data包含@tostring注解

不要随便打印sysuser,例如:system.out.println(sysuser); 任何形式的tostring()调用都不要有,否则很有可能造成循环调用,死递归。想想看,sysuser里面要查sysrole,sysrole要查sysmenu,sysmenu又要查sysrole。除非不用懒加载。

基于Spring Security前后端分离的权限控制系统问题

3. 自定义userdetails

虽然可以使用spring security自带的user,但是笔者还是强烈建议自定义一个userdetails,后面可以直接将其序列化成json缓存到redis中

package com.example.demo5.domain;
  
  import lombok.setter;
  import org.springframework.security.core.grantedauthority;
  import org.springframework.security.core.authority.simplegrantedauthority;
  import org.springframework.security.core.userdetails.user;
  import org.springframework.security.core.userdetails.userdetails;
  
  import java.util.collection;
 import java.util.set;
 
 /**
  * @author chengjiansheng
  * @date 2021/6/12
  * @see user
  * @see org.springframework.security.core.userdetails.user
  */
 @setter
 public class myuserdetails implements userdetails {
 
     private string username;
     private string password;
     private boolean enabled;
 //    private collection<? extends grantedauthority> authorities;
     private set<simplegrantedauthority> authorities;
 
     public myuserdetails(string username, string password, boolean enabled, set<simplegrantedauthority> authorities) {
         this.username = username;
         this.password = password;
         this.enabled = enabled;
         this.authorities = authorities;
     }
 
     @override
     public collection<? extends grantedauthority> getauthorities() {
         return authorities;
     }
 
     @override
     public string getpassword() {
         return password;
     }
 
     @override
     public string getusername() {
         return username;
     }
 
     @override
     public boolean isaccountnonexpired() {
         return true;
     }
 
     @override
     public boolean isaccountnonlocked() {
         return true;
     }
 
     @override
     public boolean iscredentialsnonexpired() {
         return true;
     }
 
     @override
     public boolean isenabled() {
         return enabled;
     }
 }

都自定义userdetails了,当然要自己实现userdetailsservice了。这里当时偷懒直接用自带的user,后面放缓存的时候才知道不方便。

package com.example.demo5.service;
  
  import com.example.demo5.entity.sysmenuentity;
  import com.example.demo5.entity.sysroleentity;
  import com.example.demo5.entity.sysuserentity;
  import com.example.demo5.repository.sysuserrepository;
  import org.apache.commons.lang3.stringutils;
  import org.springframework.security.core.authority.simplegrantedauthority;
  import org.springframework.security.core.userdetails.user;
 import org.springframework.security.core.userdetails.userdetails;
 import org.springframework.security.core.userdetails.userdetailsservice;
 import org.springframework.security.core.userdetails.usernamenotfoundexception;
 import org.springframework.stereotype.service;
 
 import javax.annotation.resource;
 import java.util.set;
 import java.util.stream.collectors;
 
 /**
  * @author chengjiansheng
  * @date 2021/6/12
  */
 @service
 public class myuserdetailsservice implements userdetailsservice {
     @resource
     private sysuserrepository sysuserrepository;
 
     @override
     public userdetails loaduserbyusername(string username) throws usernamenotfoundexception {
         sysuserentity sysuserentity = sysuserrepository.findbyusername(username);
         set<sysroleentity> roleset = sysuserentity.getroles();
         set<simplegrantedauthority> authorities = roleset.stream().flatmap(role->role.getmenus().stream())
                 .filter(menu-> stringutils.isnotblank(menu.getcode()))
                 .map(sysmenuentity::getcode)
                 .map(simplegrantedauthority::new)
                 .collect(collectors.toset());
         user user = new user(sysuserentity.getusername(), sysuserentity.getpassword(), authorities);
         return user;
     }
 }

算了,还是改过来吧

package com.example.demo5.service;
  
  import com.example.demo5.domain.myuserdetails;
  import com.example.demo5.entity.sysmenuentity;
  import com.example.demo5.entity.sysroleentity;
  import com.example.demo5.entity.sysuserentity;
  import com.example.demo5.repository.sysuserrepository;
  import org.apache.commons.lang3.stringutils;
  import org.springframework.security.core.authority.simplegrantedauthority;
 import org.springframework.security.core.userdetails.user;
 import org.springframework.security.core.userdetails.userdetails;
 import org.springframework.security.core.userdetails.userdetailsservice;
 import org.springframework.security.core.userdetails.usernamenotfoundexception;
 import org.springframework.stereotype.service;
 
 import javax.annotation.resource;
 import java.util.set;
 import java.util.stream.collectors;
 
 /**
  * @author chengjiansheng
  * @date 2021/6/12
  */
 @service
 public class myuserdetailsservice implements userdetailsservice {
     @resource
     private sysuserrepository sysuserrepository;
 
     @override
     public userdetails loaduserbyusername(string username) throws usernamenotfoundexception {
         sysuserentity sysuserentity = sysuserrepository.findbyusername(username);
         set<sysroleentity> roleset = sysuserentity.getroles();
         set<simplegrantedauthority> authorities = roleset.stream().flatmap(role->role.getmenus().stream())
                 .filter(menu-> stringutils.isnotblank(menu.getcode()))
                 .map(sysmenuentity::getcode)
                 .map(simplegrantedauthority::new)
                 .collect(collectors.toset());
 //        return new user(sysuserentity.getusername(), sysuserentity.getpassword(), authorities);
         return new myuserdetails(sysuserentity.getusername(), sysuserentity.getpassword(), 1==sysuserentity.getenabled(), authorities);
     }
 }

4. 自定义各种handler

登录成功

package com.example.demo5.handler;
  
  import com.alibaba.fastjson.json;
  import com.example.demo5.domain.myuserdetails;
  import com.example.demo5.domain.respresult;
  import com.example.demo5.util.jwtutils;
  import com.fasterxml.jackson.databind.objectmapper;
  import org.springframework.beans.factory.annotation.autowired;
  import org.springframework.data.redis.core.stringredistemplate;
 import org.springframework.security.core.authentication;
 import org.springframework.security.web.authentication.savedrequestawareauthenticationsuccesshandler;
 import org.springframework.stereotype.component;
 
 import javax.servlet.servletexception;
 import javax.servlet.http.httpservletrequest;
 import javax.servlet.http.httpservletresponse;
 import java.io.ioexception;
 import java.io.printwriter;
 import java.util.concurrent.timeunit;
 
 /**
  * 登录成功
  */
 @component
 public class myauthenticationsuccesshandler extends savedrequestawareauthenticationsuccesshandler {
 
     private static objectmapper objectmapper = new objectmapper();
 
     @autowired
     private stringredistemplate stringredistemplate;
 
     @override
     public void onauthenticationsuccess(httpservletrequest request, httpservletresponse response, authentication authentication) throws servletexception, ioexception {
         myuserdetails user = (myuserdetails) authentication.getprincipal();
         string username = user.getusername();
         string token = jwtutils.createtoken(username);
         stringredistemplate.opsforvalue().set("token:" + token, json.tojsonstring(user), 60, timeunit.minutes);
 
         response.setcontenttype("application/json;charset=utf-8");
         printwriter writer = response.getwriter();
         writer.write(objectmapper.writevalueasstring(new respresult<>(1, "success", token)));
         writer.flush();
         writer.close();
     }
 }

登录失败

package com.example.demo5.handler;
  
  import com.example.demo5.domain.respresult;
  import com.fasterxml.jackson.databind.objectmapper;
  import org.springframework.security.core.authenticationexception;
  import org.springframework.security.web.authentication.simpleurlauthenticationfailurehandler;
  import org.springframework.stereotype.component;
  
  import javax.servlet.servletexception;
 import javax.servlet.http.httpservletrequest;
 import javax.servlet.http.httpservletresponse;
 import java.io.ioexception;
 import java.io.printwriter;
 
 /**
  * 登录失败
  */
 @component
 public class myauthenticationfailurehandler extends simpleurlauthenticationfailurehandler {
 
     private static objectmapper objectmapper = new objectmapper();
 
     @override
     public void onauthenticationfailure(httpservletrequest request, httpservletresponse response, authenticationexception exception) throws ioexception, servletexception {
         response.setcontenttype("application/json;charset=utf-8");
         printwriter writer = response.getwriter();
         writer.write(objectmapper.writevalueasstring(new respresult<>(0, exception.getmessage(), null)));
         writer.flush();
         writer.close();
     }
 }

未登录

package com.example.demo5.handler;
  
  import com.example.demo5.domain.respresult;
  import com.fasterxml.jackson.databind.objectmapper;
  import org.springframework.security.core.authenticationexception;
  import org.springframework.security.web.authenticationentrypoint;
  import org.springframework.stereotype.component;
  
  import javax.servlet.servletexception;
 import javax.servlet.http.httpservletrequest;
 import javax.servlet.http.httpservletresponse;
 import java.io.ioexception;
 import java.io.printwriter;
 
 /**
  * 未认证(未登录)统一处理
  * @author chengjiansheng
  * @date 2021/5/7
  */
 @component
 public class myauthenticationentrypoint implements authenticationentrypoint {
 
     private static objectmapper objectmapper = new objectmapper();
 
     @override
     public void commence(httpservletrequest request, httpservletresponse response, authenticationexception authexception) throws ioexception, servletexception {
         response.setcontenttype("application/json;charset=utf-8");
         printwriter writer = response.getwriter();
         writer.write(objectmapper.writevalueasstring(new respresult<>(0, "未登录,请先登录", null)));
         writer.flush();
         writer.close();
     }
 }

未授权

package com.example.demo5.handler;
  
  import com.example.demo5.domain.respresult;
  import com.fasterxml.jackson.databind.objectmapper;
  import org.springframework.security.access.accessdeniedexception;
  import org.springframework.security.web.access.accessdeniedhandler;
  import org.springframework.stereotype.component;
  
  import javax.servlet.servletexception;
 import javax.servlet.http.httpservletrequest;
 import javax.servlet.http.httpservletresponse;
 import java.io.ioexception;
 import java.io.printwriter;
 
 @component
 public class myaccessdeniedhandler implements accessdeniedhandler {
 
     private static objectmapper objectmapper = new objectmapper();
 
     @override
     public void handle(httpservletrequest request, httpservletresponse response, accessdeniedexception accessdeniedexception) throws ioexception, servletexception {
         response.setcontenttype("application/json;charset=utf-8");
         printwriter writer = response.getwriter();
         writer.write(objectmapper.writevalueasstring(new respresult<>(0, "抱歉,您没有权限访问", null)));
         writer.flush();
         writer.close();
     }
 }

session过期

package com.example.demo5.handler;
  
  import com.example.demo5.domain.respresult;
  import com.fasterxml.jackson.databind.objectmapper;
  import org.springframework.security.web.session.sessioninformationexpiredevent;
  import org.springframework.security.web.session.sessioninformationexpiredstrategy;
  
  import javax.servlet.servletexception;
  import javax.servlet.http.httpservletresponse;
 import java.io.ioexception;
 import java.io.printwriter;
 
 public class myexpiredsessionstrategy implements sessioninformationexpiredstrategy {
 
     private static objectmapper objectmapper = new objectmapper();
 
     @override
     public void onexpiredsessiondetected(sessioninformationexpiredevent event) throws ioexception, servletexception {
         string msg = "登录超时或已在另一台机器登录,您*下线!";
         respresult respresult = new respresult(0, msg, null);
         httpservletresponse response = event.getresponse();
         response.setcontenttype("application/json;charset=utf-8");
         printwriter writer = response.getwriter();
         writer.write(objectmapper.writevalueasstring(respresult));
         writer.flush();
         writer.close();
     }
 }

退出成功

package com.example.demo5.handler;
  
  import com.fasterxml.jackson.databind.objectmapper;
  import org.springframework.beans.factory.annotation.autowired;
  import org.springframework.data.redis.core.stringredistemplate;
  import org.springframework.security.core.authentication;
  import org.springframework.security.web.authentication.logout.logoutsuccesshandler;
  import org.springframework.stereotype.component;
  
 import javax.servlet.servletexception;
 import javax.servlet.http.httpservletrequest;
 import javax.servlet.http.httpservletresponse;
 import java.io.ioexception;
 import java.io.printwriter;
 
 @component
 public class mylogoutsuccesshandler implements logoutsuccesshandler {
 
     private static objectmapper objectmapper = new objectmapper();
 
     @autowired
     private stringredistemplate stringredistemplate;
 
     @override
     public void onlogoutsuccess(httpservletrequest request, httpservletresponse response, authentication authentication) throws ioexception, servletexception {
         string token = request.getheader("token");
         stringredistemplate.delete("token:" + token);
 
         response.setcontenttype("application/json;charset=utf-8");
         printwriter printwriter = response.getwriter();
         printwriter.write(objectmapper.writevalueasstring("logout success"));
         printwriter.flush();
         printwriter.close();
     }
 }

5. token处理

现在由于前后端分离,服务端不再维持session,于是需要token来作为访问凭证

token工具类

package com.example.demo5.util;
  
  import io.jsonwebtoken.*;
  
  import java.util.date;
  import java.util.hashmap;
  import java.util.map;
  import java.util.function.function;
  
 /**
  * @author chengjiansheng
  * @date 2021/5/7
  */
 public class jwtutils {
 
     private static long token_expiration = 24 * 60 * 60 * 1000;
     private static string token_secret_key = "123456";
 
     /**
      * 生成token
      * @param subject   用户名
      * @return
      */
     public static string createtoken(string subject) {
         long currenttimemillis = system.currenttimemillis();
         date currentdate = new date(currenttimemillis);
         date expirationdate = new date(currenttimemillis + token_expiration);
 
         //  存放自定义属性,比如用户拥有的权限
         map<string, object> claims = new hashmap<>();
 
         return jwts.builder()
                 .setclaims(claims)
                 .setsubject(subject)
                 .setissuedat(currentdate)
                 .setexpiration(expirationdate)
                 .signwith(signaturealgorithm.hs512, token_secret_key)
                 .compact();
     }
 
     public static string extractusername(string token) {
         return extractclaim(token, claims::getsubject);
     }
 
     public static boolean istokenexpired(string token) {
         return extractexpiration(token).before(new date());
     }
 
     public static date extractexpiration(string token) {
         return extractclaim(token, claims::getexpiration);
     }
 
     public static <t> t extractclaim(string token, function<claims, t> claimsresolver) {
         final claims claims = extractallclaims(token);
         return claimsresolver.apply(claims);
     }
 
     private static claims extractallclaims(string token) {
         return jwts.parser().setsigningkey(token_secret_key).parseclaimsjws(token).getbody();
     }
 
 }

前后端约定登录成功以后,将token放到header中。于是,我们需要过滤器来处理请求header中的token,为此定义一个tokenfilter

package com.example.demo5.filter;
  
  import com.alibaba.fastjson.json;
  import com.example.demo5.domain.myuserdetails;
  import org.apache.commons.lang3.stringutils;
  import org.springframework.beans.factory.annotation.autowired;
  import org.springframework.data.redis.core.stringredistemplate;
  import org.springframework.security.authentication.usernamepasswordauthenticationtoken;
  import org.springframework.security.core.context.securitycontextholder;
 import org.springframework.stereotype.component;
 import org.springframework.web.filter.onceperrequestfilter;
 
 import javax.servlet.filterchain;
 import javax.servlet.servletexception;
 import javax.servlet.http.httpservletrequest;
 import javax.servlet.http.httpservletresponse;
 import java.io.ioexception;
 import java.util.concurrent.timeunit;
 
 /**
  * @author chengjiansheng
  * @date 2021/6/17
  */
 @component
 public class tokenfilter extends onceperrequestfilter {
 
     @autowired
     private stringredistemplate stringredistemplate;
 
     @override
     protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain chain) throws servletexception, ioexception {
         string token = request.getheader("token");
         system.out.println("请求头中带的token: " + token);
         string key = "token:" + token;
         if (stringutils.isnotblank(token)) {
             string value = stringredistemplate.opsforvalue().get(key);
             if (stringutils.isnotblank(value)) {
 //                string username = jwtutils.extractusername(token);
                 myuserdetails user = json.parseobject(value, myuserdetails.class);
                 if (null != user && null == securitycontextholder.getcontext().getauthentication()) {
                     usernamepasswordauthenticationtoken authenticationtoken = new usernamepasswordauthenticationtoken(user, null, user.getauthorities());
                     securitycontextholder.getcontext().setauthentication(authenticationtoken);
 
                     //  刷新token
                     //  如果生存时间小于10分钟,则再续1小时
                     long time = stringredistemplate.getexpire(key);
                     if (time < 600) {
                         stringredistemplate.expire(key, (time + 3600), timeunit.seconds);
                     }
                 }
             }
         }
 
         chain.dofilter(request, response);
     }
 }

token过滤器做了两件事,一是获取header中的token,构造usernamepasswordauthenticationtoken放入上下文中。权限可以从数据库中再查一遍,也可以直接从之前的缓存中获取。二是为token续期,即刷新token。

由于我们采用jwt生成token,因此没法中途更改token的有效期,只能将其放到redis中,通过更改redis中key的生存时间来控制token的有效期。

6. 访问控制

首先来定义资源

package com.example.demo5.controller;
  
  import org.springframework.security.access.prepost.preauthorize;
  import org.springframework.web.bind.annotation.getmapping;
  import org.springframework.web.bind.annotation.requestmapping;
  import org.springframework.web.bind.annotation.restcontroller;
  
  /**
   * @author chengjiansheng
  * @date 2021/6/12
  */
 @restcontroller
 @requestmapping("/hello")
 public class hellocontroller {
 
     @preauthorize("@myaccessdecisionservice.haspermission('hello:sayhello')")
     @getmapping("/sayhello")
     public string sayhello() {
         return "hello";
     }
 
     @preauthorize("@myaccessdecisionservice.haspermission('hello:sayhi')")
     @getmapping("/sayhi")
     public string sayhi() {
         return "hi";
     }
 }

资源的访问控制我们通过判断是否有相应的权限字符串

package com.example.demo5.service;
  
  import org.springframework.security.core.authentication;
  import org.springframework.security.core.grantedauthority;
  import org.springframework.security.core.authority.simplegrantedauthority;
  import org.springframework.security.core.context.securitycontextholder;
  import org.springframework.security.core.userdetails.userdetails;
  import org.springframework.stereotype.component;
  
 import java.util.set;
 import java.util.stream.collectors;
 
 @component("myaccessdecisionservice")
 public class myaccessdecisionservice {
 
     public boolean haspermission(string permission) {
         authentication authentication = securitycontextholder.getcontext().getauthentication();
         object principal = authentication.getprincipal();
         if (principal instanceof userdetails) {
             userdetails userdetails = (userdetails) principal;
 //            simplegrantedauthority simplegrantedauthority = new simplegrantedauthority(permission);
             set<string> set = userdetails.getauthorities().stream().map(grantedauthority::getauthority).collect(collectors.toset());
             return set.contains(permission);
         }
         return false;
     }
 }

7. 配置websecurity

package com.example.demo5.config;
  
  import com.example.demo5.filter.tokenfilter;
  import com.example.demo5.handler.*;
  import com.example.demo5.service.myuserdetailsservice;
  import org.springframework.beans.factory.annotation.autowired;
  import org.springframework.security.config.annotation.authentication.builders.authenticationmanagerbuilder;
  import org.springframework.security.config.annotation.method.configuration.enableglobalmethodsecurity;
  import org.springframework.security.config.annotation.web.builders.httpsecurity;
 import org.springframework.security.config.annotation.web.configuration.enablewebsecurity;
 import org.springframework.security.config.annotation.web.configuration.websecurityconfigureradapter;
 import org.springframework.security.config.http.sessioncreationpolicy;
 import org.springframework.security.crypto.bcrypt.bcryptpasswordencoder;
 import org.springframework.security.crypto.password.passwordencoder;
 import org.springframework.security.web.authentication.usernamepasswordauthenticationfilter;
 
 /**
  * @author chengjiansheng
  * @date 2021/6/12
  */
 @enableglobalmethodsecurity(prepostenabled = true)
 @enablewebsecurity
 public class websecurityconfig extends websecurityconfigureradapter {
 
     @autowired
     private myuserdetailsservice myuserdetailsservice;
     @autowired
     private myauthenticationsuccesshandler myauthenticationsuccesshandler;
     @autowired
     private myauthenticationfailurehandler myauthenticationfailurehandler;
     @autowired
     private tokenfilter tokenfilter;
 
     @override
     protected void configure(authenticationmanagerbuilder auth) throws exception {
         auth.userdetailsservice(myuserdetailsservice).passwordencoder(passwordencoder());
     }
 
     @override
     protected void configure(httpsecurity http) throws exception {
         http.formlogin()
 //                .usernameparameter("username")
 //                .passwordparameter("password")
 //                .loginpage("/login.html")
                 .successhandler(myauthenticationsuccesshandler)
                 .failurehandler(myauthenticationfailurehandler)
                 .and()
                 .logout().logoutsuccesshandler(new mylogoutsuccesshandler())
                 .and()
                 .authorizerequests()
                 .antmatchers("/demo/login").permitall()
 //                .antmatchers("/css/**", "/js/**", "/**/images/*.*").permitall()
 //                .regexmatchers(".+[.]jpg").permitall()
 //                .mvcmatchers("/hello").servletpath("/demo").permitall()
                 .anyrequest().authenticated()
                 .and()
                 .exceptionhandling()
                 .accessdeniedhandler(new myaccessdeniedhandler())
                 .authenticationentrypoint(new myauthenticationentrypoint())
                 .and()
                 .sessionmanagement().sessioncreationpolicy(sessioncreationpolicy.stateless)
                 .maximumsessions(1)
                 .maxsessionspreventslogin(false)
                 .expiredsessionstrategy(new myexpiredsessionstrategy());
 
         http.addfilterbefore(tokenfilter, usernamepasswordauthenticationfilter.class);
 
         http.csrf().disable();
     }
 
     public passwordencoder passwordencoder() {
         return new bcryptpasswordencoder();
     }
 
     public static void main(string[] args) {
         system.out.println(new bcryptpasswordencoder().encode("123456"));
     }
 }

注意,我们将自定义的tokenfilter放到usernamepasswordauthenticationfilter之前

所有过滤器的顺序可以查看 org.springframework.security.config.annotation.web.builders.filtercomparator 或者org.springframework.security.config.annotation.web.builders.filterorderregistration

8. 看效果

基于Spring Security前后端分离的权限控制系统问题

基于Spring Security前后端分离的权限控制系统问题

基于Spring Security前后端分离的权限控制系统问题

基于Spring Security前后端分离的权限控制系统问题

基于Spring Security前后端分离的权限控制系统问题

基于Spring Security前后端分离的权限控制系统问题

9. 补充:手机号+短信验证码登录

参照org.springframework.security.authentication.usernamepasswordauthenticationtoken写一个短信认证token

package com.example.demo5.filter;
  
  import org.springframework.security.authentication.abstractauthenticationtoken;
  import org.springframework.security.core.grantedauthority;
  import org.springframework.security.core.springsecuritycoreversion;
  import org.springframework.util.assert;
  
  import java.util.collection;
  
 /**
  * @author chengjiansheng
  * @date 2021/5/12
  */
 public class smscodeauthenticationtoken extends abstractauthenticationtoken {
 
     private static final long serialversionuid = springsecuritycoreversion.serial_version_uid;
 
     private final object principal;
 
     private object credentials;
 
     public smscodeauthenticationtoken(object principal, object credentials) {
         super(null);
         this.principal = principal;
         this.credentials = credentials;
         setauthenticated(false);
     }
 
     public smscodeauthenticationtoken(object principal, object credentials, collection<? extends grantedauthority> authorities) {
         super(authorities);
         this.principal = principal;
         this.credentials = credentials;
         super.setauthenticated(true);
     }
 
     @override
     public object getcredentials() {
         return credentials;
     }
 
     @override
     public object getprincipal() {
         return principal;
     }
 
     @override
     public void setauthenticated(boolean authenticated) {
         assert.istrue(!authenticated, "cannot set this token to trusted - use constructor which takes a grantedauthority list instead");
         super.setauthenticated(false);
     }
 
     @override
     public void erasecredentials() {
         super.erasecredentials();
     }
 }

参照org.springframework.security.authentication.dao.daoauthenticationprovider写一个自己的短信认证provider

package com.example.demo5.filter;
 
  import com.example.demo.service.myuserdetailsservice;
  import org.apache.commons.lang3.stringutils;
  import org.springframework.security.authentication.authenticationprovider;
  import org.springframework.security.authentication.badcredentialsexception;
  import org.springframework.security.core.authentication;
  import org.springframework.security.core.authenticationexception;
  import org.springframework.security.core.userdetails.userdetails;
 
 /**
  * @author chengjiansheng
  * @date 2021/5/12
  */
 public class smsauthenticationprovider implements authenticationprovider {
 
     private myuserdetailsservice myuserdetailsservice;
 
     @override
     public authentication authenticate(authentication authentication) throws authenticationexception {
         //  校验验证码
         additionalauthenticationchecks((smscodeauthenticationtoken) authentication);
 
         //  校验手机号
         string mobile = authentication.getprincipal().tostring();
 
         userdetails userdetails = myuserdetailsservice.loaduserbymobile(mobile);
 
         if (null == userdetails) {
             throw new badcredentialsexception("手机号不存在");
         }
 
         //  创建认证成功的authentication对象
         smscodeauthenticationtoken result = new smscodeauthenticationtoken(userdetails, userdetails.getauthorities());
         result.setdetails(authentication.getdetails());
 
         return result;
     }
 
     protected void additionalauthenticationchecks(smscodeauthenticationtoken authentication) throws authenticationexception {
         if (authentication.getcredentials() == null) {
             throw new badcredentialsexception("验证码不能为空");
         }
         string mobile = authentication.getprincipal().tostring();
         string smscode = authentication.getcredentials().tostring();
 
         //  从session或者redis中获取相应的验证码
         string smscodeinsessionkey = "sms_code_" + mobile;
 //        string verificationcode = sessionstrategy.getattribute(servletwebrequest, smscodeinsessionkey);
 //        string verificationcode = stringredistemplate.opsforvalue().get(smscodeinsessionkey);
         string verificationcode = "1234";
 
         if (stringutils.isblank(verificationcode)) {
             throw new badcredentialsexception("短信验证码不存在,请重新发送!");
         }
         if (!smscode.equalsignorecase(verificationcode)) {
             throw new badcredentialsexception("验证码错误!");
         }
 
         //todo  清除session或者redis中获取相应的验证码
     }
 
     @override
     public boolean supports(class<?> authentication) {
         return (smscodeauthenticationtoken.class.isassignablefrom(authentication));
     }
 
     public myuserdetailsservice getmyuserdetailsservice() {
         return myuserdetailsservice;
     }
 
     public void setmyuserdetailsservice(myuserdetailsservice myuserdetailsservice) {
         this.myuserdetailsservice = myuserdetailsservice;
     }
 }

参照org.springframework.security.web.authentication.usernamepasswordauthenticationfilter写一个短信认证处理的过滤器

package com.example.demo.filter;
  
  import org.springframework.security.authentication.authenticationmanager;
  import org.springframework.security.authentication.authenticationserviceexception;
  import org.springframework.security.core.authentication;
  import org.springframework.security.core.authenticationexception;
  import org.springframework.security.web.authentication.abstractauthenticationprocessingfilter;
  import org.springframework.security.web.util.matcher.antpathrequestmatcher;
  
 import javax.servlet.servletexception;
 import javax.servlet.http.httpservletrequest;
 import javax.servlet.http.httpservletresponse;
 import java.io.ioexception;
 
 /**
  * @author chengjiansheng
  * @date 2021/5/12
  */
 public class smsauthenticationfilter extends abstractauthenticationprocessingfilter {
 
     public static final string spring_security_form_mobile_key = "mobile";
 
     public static final string spring_security_form_password_key = "smscode";
 
     private static final antpathrequestmatcher default_ant_path_request_matcher = new antpathrequestmatcher("/login/mobile", "post");
 
     private string usernameparameter = spring_security_form_mobile_key;
 
     private string passwordparameter = spring_security_form_password_key;
 
     private boolean postonly = true;
 
     public smsauthenticationfilter() {
         super(default_ant_path_request_matcher);
     }
 
     public smsauthenticationfilter(authenticationmanager authenticationmanager) {
         super(default_ant_path_request_matcher, authenticationmanager);
     }
 
     @override
     public authentication attemptauthentication(httpservletrequest request, httpservletresponse response) throws authenticationexception, ioexception, servletexception {
         if (postonly && !request.getmethod().equals("post")) {
             throw new authenticationserviceexception("authentication method not supported: " + request.getmethod());
         }
 
         string mobile = obtainmobile(request);
         mobile = (mobile != null) ? mobile : "";
         mobile = mobile.trim();
         string smscode = obtainpassword(request);
         smscode = (smscode != null) ? smscode : "";
 
         smscodeauthenticationtoken authrequest = new smscodeauthenticationtoken(mobile, smscode);
 
         setdetails(request, authrequest);
 
         return this.getauthenticationmanager().authenticate(authrequest);
     }
 
     private string obtainmobile(httpservletrequest request) {
         return request.getparameter(this.usernameparameter);
     }
 
     private string obtainpassword(httpservletrequest request) {
         return request.getparameter(this.passwordparameter);
     }
 
     protected void setdetails(httpservletrequest request, smscodeauthenticationtoken authrequest) {
         authrequest.setdetails(this.authenticationdetailssource.builddetails(request));
     }
 }

在websecurity中进行配置

package com.example.demo.config;
  
  import com.example.demo.filter.smsauthenticationfilter;
  import com.example.demo.filter.smsauthenticationprovider;
  import com.example.demo.handler.myauthenticationfailurehandler;
  import com.example.demo.handler.myauthenticationsuccesshandler;
  import com.example.demo.service.myuserdetailsservice;
  import org.springframework.beans.factory.annotation.autowired;
  import org.springframework.security.authentication.authenticationmanager;
 import org.springframework.security.config.annotation.securityconfigureradapter;
 import org.springframework.security.config.annotation.web.builders.httpsecurity;
 import org.springframework.security.web.defaultsecurityfilterchain;
 import org.springframework.security.web.authentication.usernamepasswordauthenticationfilter;
 import org.springframework.stereotype.component;
 
 /**
  * @author chengjiansheng
  * @date 2021/5/12
  */
 @component
 public class smsauthenticationconfig extends securityconfigureradapter<defaultsecurityfilterchain, httpsecurity> {
 
     @autowired
     private myuserdetailsservice myuserdetailsservice;
     @autowired
     private myauthenticationsuccesshandler myauthenticationsuccesshandler;
     @autowired
     private myauthenticationfailurehandler myauthenticationfailurehandler;
 
     @override
     public void configure(httpsecurity http) throws exception {
         smsauthenticationfilter smsauthenticationfilter = new smsauthenticationfilter();
         smsauthenticationfilter.setauthenticationmanager(http.getsharedobject(authenticationmanager.class));
         smsauthenticationfilter.setauthenticationsuccesshandler(myauthenticationsuccesshandler);
         smsauthenticationfilter.setauthenticationfailurehandler(myauthenticationfailurehandler);
 
         smsauthenticationprovider smsauthenticationprovider = new smsauthenticationprovider();
         smsauthenticationprovider.setmyuserdetailsservice(myuserdetailsservice);
 
         http.authenticationprovider(smsauthenticationprovider)
                 .addfilterafter(smsauthenticationfilter, usernamepasswordauthenticationfilter.class);
     }
 }
 http.apply(smsauthenticationconfig);

以上就是基于 spring security前后端分离的权限控制系统的详细内容,更多关于spring security权限控制系统的资料请关注其它相关文章!