基于Spring Security前后端分离的权限控制系统问题
前后端分离的项目,前端有菜单(menu),后端有api(backendapi),一个menu对应的页面有n个api接口来支持,本文介绍如何基于spring security前后端分离的权限控制系统问题。
话不多说,入正题。一个简单的权限控制系统需要考虑的问题如下:
- 权限如何加载
- 权限匹配规则
- 登录
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. 建表并生成相应的实体类
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。除非不用懒加载。
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. 看效果
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权限控制系统的资料请关注其它相关文章!