spring security实现动态路由权限控制
主要步骤如下:
- 1、securityuser implements userdetails 接口中的方法
- 2、自定义认证:userdetailsserviceimpl implements userdetailsservice
- 3、添加登录过滤器loginfilter extends onceperrequestfilter
每次访问接口都会经过此,我们可以在这里记录请求参数、响应内容,或者处理前后端分离情况下, 以token换用户权限信息,token是否过期,请求头类型是否正确,防止非法请求等等
- 4、动态权限过滤器,用于实现基于路径的动态权限过滤:securityfilter extends abstractsecurityinterceptor implements filter
- 5、未登录访问控制类:adminauthenticationentrypoint implements authenticationentrypoint
- 6、获取访问url所需要的角色信息类:urlfilterinvocationsecuritymetadatasource implements filterinvocationsecuritymetadatasource
- 7、权限认证处理类:urlaccessdecisionmanager implements accessdecisionmanager,认证失败抛出:accessdeniedexception 异常
- 8、权限认证失败后的处理类:urlaccessdeniedhandler implements accessdeniedhandler
- 9、核心配置securityconfig
代码实现
1、securityuser implements userdetails 接口中的方法
package com.example.security.url.entity; import lombok.data; import lombok.tostring; import lombok.extern.slf4j.slf4j; import org.springframework.security.core.grantedauthority; import org.springframework.security.core.authority.simplegrantedauthority; import org.springframework.security.core.userdetails.userdetails; import org.springframework.util.collectionutils; import java.util.arraylist; import java.util.collection; import java.util.list; /** * @author deyou kong * @description security验证用户 * @date 2023/2/9 3:01 下午 */ @data @slf4j @tostring public class securityuser implements userdetails { /** * 用户信息 */ private user user; /** * 用户拥有的角色列表 */ private list<role> roles; public securityuser() { } public securityuser(user user) { if (user != null) { this.user = user; } } public securityuser(user user, list<role> rolelist) { if (user != null) { this.user = user; this.roles = rolelist; } } /** * 获取当前用户所具有的角色 * @return 返回角色列表 list<role.getcode()> */ @override public collection<? extends grantedauthority> getauthorities() { collection<grantedauthority> authorities = new arraylist<>(); if (!collectionutils.isempty(this.roles)) { for (role role : this.roles) { simplegrantedauthority authority = new simplegrantedauthority(role.getcode()); authorities.add(authority); } } return authorities; } @override public string getpassword() { return user.getpassword(); } @override public string getusername() { return user.getusername(); } @override public boolean isaccountnonexpired() { return true; } @override public boolean isaccountnonlocked() { return true; } @override public boolean iscredentialsnonexpired() { return true; } @override public boolean isenabled() { return user.getstatus() == 1 ? true: false; } }
2、自定义认证:userdetailsserviceimpl implements userdetailsservice
package com.example.security.url.service.impl; import com.baomidou.mybatisplus.core.conditions.query.lambdaquerywrapper; import com.example.security.url.constants.resultconstant; import com.example.security.url.dao.rolemapper; import com.example.security.url.dao.usermapper; import com.example.security.url.dao.userrolemapper; import com.example.security.url.entity.*; import lombok.extern.slf4j.slf4j; 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 org.springframework.util.collectionutils; import javax.annotation.resource; import java.util.arraylist; import java.util.list; import java.util.set; import java.util.stream.collectors; @service @slf4j public class userdetailsserviceimpl implements userdetailsservice { @resource usermapper usermapper; @resource userrolemapper userrolemapper; @resource rolemapper rolemapper; @override public userdetails loaduserbyusername(string username) throws usernamenotfoundexception { log.info("userdetailsservice实现类"); lambdaquerywrapper<user> querywrapper = new lambdaquerywrapper<>(); querywrapper.eq(user::getusername, username); user user = usermapper.selectone(querywrapper); //如果用户被禁用,则不再查询权限表 if (user == null){ // 抛出异常,会被loginfailhandlerentrypoint捕获 throw new usernamenotfoundexception(resultconstant.user_not_exist); //return null; } return new securityuser(user, getuserroles(user.getid())); } /** * 根据用户id获取角色权限信息 * * @param userid * @return */ private list<role> getuserroles(integer userid) { lambdaquerywrapper<userrole> userrolelambdaquerywrapper = new lambdaquerywrapper<>(); userrolelambdaquerywrapper.eq(userrole::getuserid, userid); list<userrole> userroles = userrolemapper.selectlist(userrolelambdaquerywrapper); // 判断用户有没有角色,没有角色,直接返回空列表 if (collectionutils.isempty(userroles)){ return new arraylist<>(); } set<integer> roleidset = userroles.stream().map(userrole::getroleid).collect(collectors.toset()); list<role> roles = rolemapper.selectbatchids(roleidset); if (collectionutils.isempty(roles)){ return new arraylist<>(); } return roles; } }
3、添加登录过滤器loginfilter extends onceperrequestfilter
每次访问接口都会经过此,我们可以在这里记录请求参数、响应内容等日志,或者处理前后端分离情况下,以token换用户权限信息,token是否过期,请求头类型是否正确,防止非法请求等等
package com.example.security.url.filter; import com.example.security.url.common.result.commonresult; import com.example.security.url.exception.loginexception; import com.example.security.url.property.ignoreurlsconfig; import com.example.security.url.constants.resultconstant; import com.example.security.url.utils.jwttokenutil; import com.example.security.url.utils.responseutils; import lombok.sneakythrows; import lombok.extern.slf4j.slf4j; import org.apache.commons.lang3.stringutils; import org.springframework.beans.factory.annotation.value; import org.springframework.security.access.accessdeniedexception; import org.springframework.security.authentication.usernamepasswordauthenticationtoken; import org.springframework.security.core.authenticationexception; import org.springframework.security.core.context.securitycontextholder; import org.springframework.security.core.userdetails.userdetails; import org.springframework.security.core.userdetails.userdetailsservice; import org.springframework.security.core.userdetails.usernamenotfoundexception; import org.springframework.security.web.authentication.webauthenticationdetailssource; import org.springframework.util.antpathmatcher; import org.springframework.util.pathmatcher; import org.springframework.web.filter.onceperrequestfilter; import javax.annotation.resource; import javax.servlet.filterchain; import javax.servlet.servletexception; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import java.io.ioexception; /** * 请求的httpservletrequest流只能读一次,下一次就不能读取了, * 因此这里要使用自定义的multireadhttpservletrequest工具解决流只能读一次的问题 * * @author deyou kong * @description 用户登录鉴权过滤器 filter * @date 2023/2/10 2:25 下午 */ @slf4j public class loginfilter extends onceperrequestfilter { @resource private userdetailsservice userdetailsservice; @resource private jwttokenutil jwttokenutil; @resource ignoreurlsconfig ignoreurlsconfig; @value("${jwt.tokenheader}") private string tokenheader; @value("${jwt.tokentype}") private string tokentype; @value("${server.servlet.context-path}") private string contextpath; @sneakythrows @override protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain chain) throws servletexception, ioexception { string requesturi = request.getrequesturi(); log.info("loginfilter -> dofilterinternal,请求url:{}", requesturi); // 如果requesturi在白名单中直接放行 try { pathmatcher pathmatcher = new antpathmatcher(); for (string url : ignoreurlsconfig.geturls()) { string requesturl = contextpath + url; if (pathmatcher.match((requesturl), requesturi)) { chain.dofilter(request, response); return; } } // 验证token string token = request.getheader(tokenheader); if (stringutils.isallblank(token)){ throw new loginexception(resultconstant.not_token); } if (!token.startswith(tokentype)){ throw new loginexception(resultconstant.token_reg_fail); } string authtoken = token.substring(tokentype.length()); if (jwttokenutil.istokenexpired(authtoken)){ throw new loginexception(resultconstant.token_invalid); } string username = jwttokenutil.getusernamefromtoken(authtoken); //if (username != null && securitycontextholder.getcontext().getauthentication() == null) { if (username != null) { userdetails userdetails = userdetailsservice.loaduserbyusername(username); if (userdetails != null) { // token 中的用户在数据库中查询到数据,开始进行密码验证 usernamepasswordauthenticationtoken authentication = new usernamepasswordauthenticationtoken(userdetails, null, userdetails.getauthorities()); authentication.setdetails(new webauthenticationdetailssource().builddetails(request)); securitycontextholder.getcontext().setauthentication(authentication); chain.dofilter(request, response); return; } } } catch (loginexception e) { commonresult<string> result = commonresult.loginfailed(e.getmessage()); responseutils.out(response, result); }catch (exception e){ e.printstacktrace(); commonresult<string> result = commonresult.loginfailed(resultconstant.sys_error); responseutils.out(response, result); return ; } } }
4、动态权限过滤器,用于实现基于路径的动态权限过滤:securityfilter extends abstractsecurityinterceptor implements filter
package com.example.security.url.filter; import com.example.security.url.url.urlaccessdecisionmanager; import com.example.security.url.property.ignoreurlsconfig; import com.example.security.url.url.urlfilterinvocationsecuritymetadatasource; import lombok.extern.slf4j.slf4j; import org.springframework.beans.factory.annotation.value; import org.springframework.http.httpmethod; import org.springframework.security.access.intercept.abstractsecurityinterceptor; import org.springframework.security.access.intercept.interceptorstatustoken; import org.springframework.security.web.filterinvocation; import org.springframework.util.antpathmatcher; import org.springframework.util.pathmatcher; import javax.annotation.resource; import javax.servlet.*; import javax.servlet.http.httpservletrequest; import java.io.ioexception; /** * 动态权限过滤器,用于实现基于路径的动态权限过滤 */ @slf4j public class securityfilter extends abstractsecurityinterceptor implements filter { @resource private urlfilterinvocationsecuritymetadatasource urlfilterinvocationsecuritymetadatasource; @resource private ignoreurlsconfig ignoreurlsconfig; @value("${server.servlet.context-path}") private string contextpath; @resource public void setaccessdecisionmanager(urlaccessdecisionmanager urlaccessdecisionmanager) { super.setaccessdecisionmanager(urlaccessdecisionmanager); } @override public void init(filterconfig filterconfig) { } @override public void dofilter(servletrequest servletrequest, servletresponse servletresponse, filterchain filterchain) throws ioexception, servletexception { httpservletrequest request = (httpservletrequest) servletrequest; filterinvocation fi = new filterinvocation(servletrequest, servletresponse, filterchain); log.info("securityfilter动态权限过滤器,用于实现基于路径的动态权限过滤"); /** * 仿照onceperrequestfilter,解决filter执行两次的问题 * 执行两次原因:securityconfig中,@bean和addfilter相当于向容器注入了两次 * 解决办法:1是去掉@bean,但filter中若有引用注入容器的其它资源,则会报错 * 2就是request中保存一个attribute来判断该请求是否已执行过 */ string alreadyfilteredattributename = getalreadyfilteredattributename(); boolean hasalreadyfilteredattribute = request.getattribute(alreadyfilteredattributename) != null; if (hasalreadyfilteredattribute) { fi.getchain().dofilter(fi.getrequest(), fi.getresponse()); return; } request.setattribute(alreadyfilteredattributename, boolean.true); //options请求直接放行 if (request.getmethod().equals(httpmethod.options.tostring())) { fi.getchain().dofilter(fi.getrequest(), fi.getresponse()); return; } //白名单请求直接放行 pathmatcher pathmatcher = new antpathmatcher(); for (string path : ignoreurlsconfig.geturls()) { if (pathmatcher.match(contextpath + path, request.getrequesturi())) { fi.getchain().dofilter(fi.getrequest(), fi.getresponse()); return; } } //此处会调用accessdecisionmanager中的decide方法进行鉴权操作 interceptorstatustoken token = super.beforeinvocation(fi); try { fi.getchain().dofilter(fi.getrequest(), fi.getresponse()); } finally { super.afterinvocation(token, null); } } @override public void destroy() { urlfilterinvocationsecuritymetadatasource.cleardatasource(); } @override public class<?> getsecureobjectclass() { return filterinvocation.class; } @override public urlfilterinvocationsecuritymetadatasource obtainsecuritymetadatasource() { log.info("securityfilter返回urlfilterinvocationsecuritymetadatasource对象"); return urlfilterinvocationsecuritymetadatasource; } protected string getalreadyfilteredattributename() { return this.getclass().getname() + ".filtered"; } }
5、未登录访问控制类:adminauthenticationentrypoint implements authenticationentrypoint
package com.example.security.url.filter; import com.alibaba.fastjson.json; import com.example.security.url.common.result.commonresult; import com.example.security.url.utils.responseutils; import lombok.extern.slf4j.slf4j; import org.springframework.security.core.authenticationexception; import org.springframework.security.web.authenticationentrypoint; import javax.servlet.servletexception; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import java.io.ioexception; /** * 在实现 userdetailsservice 接口的类中抛出 org.springframework.security.core.userdetails.usernamenotfoundexception 异常都会被此类捕获 * @author deyou kong * @description 登录失败处理类/未登录, * @date 2023/2/10 2:19 下午 */ @slf4j public class loginfailhandlerentrypoint implements authenticationentrypoint { @override public void commence(httpservletrequest request, httpservletresponse response, authenticationexception authexception) throws ioexception, servletexception { log.warn("loginfailhandlerentrypoint 登录失败处理类"); responseutils.out(response, commonresult.loginfailed(authexception.getlocalizedmessage())); } }
responseutils 工具类文末附上
6、获取访问url所需要的角色信息类:urlfilterinvocationsecuritymetadatasource implements filterinvocationsecuritymetadatasource
package com.example.security.url.url; import com.baomidou.mybatisplus.core.conditions.query.lambdaquerywrapper; import com.example.security.url.property.ignoreurlsconfig; import com.example.security.url.constants.resultconstant; import com.example.security.url.dao.permissionmapper; import com.example.security.url.dao.rolemapper; import com.example.security.url.dao.rolepermissionmapper; import com.example.security.url.entity.permission; import com.example.security.url.entity.role; import com.example.security.url.entity.rolepermission; import lombok.extern.slf4j.slf4j; import org.springframework.security.access.configattribute; import org.springframework.security.access.securityconfig; import org.springframework.security.web.filterinvocation; import org.springframework.security.web.access.intercept.filterinvocationsecuritymetadatasource; import org.springframework.util.antpathmatcher; import org.springframework.util.collectionutils; import javax.annotation.resource; import java.util.collection; import java.util.list; import java.util.set; import java.util.stream.collectors; /** * @author deyou kong * @description 访问url需要的角色权限 * @date 2023/2/10 4:19 下午 */ @slf4j public class urlfilterinvocationsecuritymetadatasource implements filterinvocationsecuritymetadatasource { /** * 正则匹配匹配 */ antpathmatcher pathmatcher = new antpathmatcher(); @resource permissionmapper permissionmapper; @resource rolepermissionmapper rolepermissionmapper; @resource rolemapper rolemapper; @resource ignoreurlsconfig ignoreurlsconfig; private list<configattribute> allconfigattributes; public void cleardatasource() { allconfigattributes.clear(); allconfigattributes = null; } /*** * 返回该url所需要的用户权限信息 * * @param object: 储存请求url信息 * @return: null:标识不需要任何权限都可以访问 */ @override public collection<configattribute> getattributes(object object) throws illegalargumentexception { log.info("urlfilterinvocationsecuritymetadatasource获取请求url所需角色"); // 获取当前请求url string requesturl = ((filterinvocation) object).getrequesturl(); int index = requesturl.indexof("?"); if (index != -1){ requesturl = requesturl.substring(0, index); } // 白名单,设置需要的角色为null for (string url : ignoreurlsconfig.geturls()) { if (url.equals(requesturl) || pathmatcher.match(url , requesturl)) { return null; } } // 数据库中所有的菜单 list<permission> permissionlist = permissionmapper.selectlist(null); if (collectionutils.isempty(permissionlist)){ return null; } for (permission permission : permissionlist) { // 与请求地址进行匹配,获取该url所对应的权限 if (pathmatcher.match(permission.geturl()+"/**", requesturl)){ list<rolepermission> permissions = rolepermissionmapper.selectlist(new lambdaquerywrapper<rolepermission>().eq(rolepermission::getpermissionid, permission.getid())); if (!collectionutils.isempty(permissions)){ set<integer> roleidset = permissions.stream().map(rolepermission::getroleid).collect(collectors.toset()); list<role> rolelist = rolemapper.selectbatchids(roleidset); list<string> rolestringlist = rolelist.stream().map(role::getcode).collect(collectors.tolist()); // 保存该url对应角色权限信息 return securityconfig.createlist(rolestringlist.toarray(new string[rolestringlist.size()])); } } } // 如果数据中没有找到相应url资源则为无权限访问 return securityconfig.createlist(resultconstant.request_forbidden_role); } @override public collection<configattribute> getallconfigattributes() { return null; } @override public boolean supports(class<?> aclass) { return filterinvocation.class.isassignablefrom(aclass); } }
7、权限认证处理类:urlaccessdecisionmanager implements accessdecisionmanager,认证失败抛出:accessdeniedexception 异常
package com.example.security.url.url; import com.example.security.url.constants.resultconstant; import lombok.extern.slf4j.slf4j; import org.springframework.security.access.accessdecisionmanager; import org.springframework.security.access.accessdeniedexception; import org.springframework.security.access.configattribute; import org.springframework.security.authentication.insufficientauthenticationexception; import org.springframework.security.core.authentication; import org.springframework.security.core.grantedauthority; import org.springframework.stereotype.component; import java.util.collection; /** * @author deyou kong * @description 权限认证处理类 * @date 2023/2/10 4:37 下午 */ @slf4j public class urlaccessdecisionmanager implements accessdecisionmanager { /** * * @param authentication * @param o * @param configattributes url所需要的角色权限列表:string[],urlroleneedfilterinvocationsecuritymetadatasource.getattributes返回的对象 * @throws accessdeniedexception * @throws insufficientauthenticationexception */ @override public void decide(authentication authentication, object o, collection<configattribute> configattributes) throws accessdeniedexception, insufficientauthenticationexception { log.info("urlaccessdecisionmanager --- > decide"); // 遍历角色 for (configattribute configattribute : configattributes) { // 当前url请求需要的权限 string needrole = configattribute.getattribute(); if (needrole.equals(resultconstant.request_forbidden_role)){ throw new accessdeniedexception(resultconstant.request_forbidden); } // 只要包含其中一个角色即可访问 collection<? extends grantedauthority> authorities = authentication.getauthorities(); for (grantedauthority authority : authorities) { if (authority.getauthority().equals(needrole)) { return; } } } throw new accessdeniedexception(resultconstant.request_forbidden); } @override public boolean supports(configattribute configattribute) { return true; } @override public boolean supports(class<?> aclass) { return true; } }
8、权限认证失败后的处理类:urlaccessdeniedhandler implements accessdeniedhandler
package com.example.security.url.url; import com.example.security.url.common.result.commonresult; import com.example.security.url.constants.resultconstant; import com.example.security.url.utils.responseutils; import lombok.extern.slf4j.slf4j; import org.springframework.security.access.accessdeniedexception; import org.springframework.security.web.access.accessdeniedhandler; import javax.servlet.servletexception; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import java.io.ioexception; /** * 在实现 accessdecisionmanager 接口中抛出 org.springframework.security.access.accessdeniedexception 异常会被这里捕获 * @author deyou kong * @description 权限认证失败处理类handler * @date 2023/2/10 4:53 下午 */ @slf4j public class urlaccessdeniedhandler implements accessdeniedhandler { @override public void handle(httpservletrequest httpservletrequest, httpservletresponse httpservletresponse, accessdeniedexception e) throws ioexception, servletexception { log.info("urlaccessdeniedhandler权限认证失败处理类"); responseutils.out(httpservletresponse, commonresult.forbidden(e.getlocalizedmessage())); } }
9、核心配置securityconfig
package com.example.security.url.config; import com.example.security.url.filter.loginfilter; import com.example.security.url.filter.securityfilter; import com.example.security.url.filter.loginfailhandlerentrypoint; import com.example.security.url.url.urlaccessdecisionmanager; import com.example.security.url.url.urlaccessdeniedhandler; import com.example.security.url.property.ignoreurlsconfig; import com.example.security.url.url.urlfilterinvocationsecuritymetadatasource; import com.example.security.url.utils.md5passwordencoder; import lombok.extern.slf4j.slf4j; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.http.httpmethod; import org.springframework.security.config.annotation.objectpostprocessor; 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.builders.websecurity; import org.springframework.security.config.annotation.web.configuration.enablewebsecurity; import org.springframework.security.config.annotation.web.configuration.websecurityconfigureradapter; import org.springframework.security.config.annotation.web.configurers.expressionurlauthorizationconfigurer; import org.springframework.security.crypto.password.passwordencoder; import org.springframework.security.web.access.intercept.filtersecurityinterceptor; import org.springframework.security.web.authentication.usernamepasswordauthenticationfilter; import javax.annotation.resource; @configuration @enablewebsecurity @enableglobalmethodsecurity(prepostenabled = true) @slf4j public class securityconfig extends websecurityconfigureradapter { @resource ignoreurlsconfig ignoreurlsconfig; @override protected void configure(httpsecurity http) throws exception { expressionurlauthorizationconfigurer<httpsecurity>.expressionintercepturlregistry registry = http.antmatcher("/**").authorizerequests(); // 禁用csrf 开启跨域 http.csrf().disable().cors(); // 未登录认证异常 http.exceptionhandling().authenticationentrypoint(loginfailhandlerentrypoint()); // 登录过后访问无权限的接口时自定义403响应内容 http.exceptionhandling().accessdeniedhandler(urlaccessdeniedhandler()); // url权限认证处理 registry.withobjectpostprocessor(new objectpostprocessor<filtersecurityinterceptor>() { @override public <o extends filtersecurityinterceptor> o postprocess(o o) { o.setsecuritymetadatasource(urlfilterinvocationsecuritymetadatasource()); o.setaccessdecisionmanager(urlaccessdecisionmanager()); return o; } }); // options(选项):查找适用于一个特定网址资源的通讯选择。 在不需执行具体的涉及数据传输的动作情况下, 允许客户端来确定与资源相关的选项以及 / 或者要求, 或是一个服务器的性能 registry.antmatchers(httpmethod.options, "/**").denyall(); // 自动登录 - cookie储存方式 registry.and().rememberme(); // 其余所有请求都需要认证 registry.anyrequest().authenticated(); // 防止iframe 造成跨域 registry.and().headers().frameoptions().disable(); // 自定义过滤器在登录时认证用户名、密码 http.addfilterat(loginfilter(), usernamepasswordauthenticationfilter.class) .addfilterbefore(securityfilter(), filtersecurityinterceptor.class); } /** * 忽略拦截url或静态资源文件夹 - web.ignoring(): 会直接过滤该url - 将不会经过spring security过滤器链 * http.permitall(): 不会绕开springsecurity验证,相当于是允许该路径通过 * @param web * @throws exception */ @override public void configure(websecurity web) throws exception { web.ignoring().antmatchers(httpmethod.get, "/favicon.ico", "/*.html", "/**/*.css", "/**/*.js"); } @override protected void configure(authenticationmanagerbuilder auth) throws exception { auth.userdetailsservice(userdetailsservice()).passwordencoder(passwordencoder()); } /** * 登录过滤器 */ @bean public loginfilter loginfilter(){ return new loginfilter(); } /** * 登录失败处理类 */ @bean public loginfailhandlerentrypoint loginfailhandlerentrypoint(){ return new loginfailhandlerentrypoint(); }; /** * 获取访问url所需要的角色信息 */ @bean public urlfilterinvocationsecuritymetadatasource urlfilterinvocationsecuritymetadatasource(){ return new urlfilterinvocationsecuritymetadatasource(); }; /** * 认证权限处理 - 将可以请求url的角色权限与当前登录用户的角色做对比,如果包含其中一个角色即可正常访问 */ @bean public urlaccessdecisionmanager urlaccessdecisionmanager(){ return new urlaccessdecisionmanager(); }; /** * 自定义访问无权限接口时403响应内容 */ @bean public urlaccessdeniedhandler urlaccessdeniedhandler(){ return new urlaccessdeniedhandler(); }; @bean public securityfilter securityfilter() { return new securityfilter(); } /** * 密码加密类 * @return */ @bean public passwordencoder passwordencoder() { return new md5passwordencoder(); } }
其他工具类
1、自定义异常
package com.example.security.url.exception; import lombok.data; /** * @author deyou kong * @description 登录异常 * @date 2023/2/13 9:18 上午 */ @data public class loginexception extends runtimeexception{ private string message; public loginexception(string message){ this.message = message; } }
2、读取配置文件配置
package com.example.security.url.property; import lombok.data; import org.springframework.boot.context.properties.configurationproperties; import org.springframework.stereotype.component; import java.util.list; @data @component @configurationproperties(prefix = "secure.ignored") public class ignoreurlsconfig { private list<string> urls; }
3、md5加密工具类
package com.example.security.url.utils; import java.math.biginteger; import java.security.messagedigest; import java.security.nosuchalgorithmexception; /** * @author deyou kong * @description md5算法 * @date 2023/2/10 7:16 下午 */ public class md5utils { /** * 使用md5的算法进行加密 */ public static string encode(string plaintext) { byte[] secretbytes = null; try { secretbytes = messagedigest.getinstance("md5").digest( plaintext.getbytes()); } catch (nosuchalgorithmexception e) { throw new runtimeexception("没有md5这个算法!"); } string md5code = new biginteger(1, secretbytes).tostring(16);// 16进制数字 // 如果生成数字未满32位,需要前面补0 for (int i = 0; i < 32 - md5code.length(); i++) { md5code = "0" + md5code; } return md5code; } }
4、md5passwordencoder
package com.example.security.url.utils; import lombok.extern.slf4j.slf4j; import org.springframework.security.crypto.password.passwordencoder; /** * @author deyou kong * @description * @date 2023/2/10 7:16 下午 */ @slf4j public class md5passwordencoder implements passwordencoder { @override public boolean matches(charsequence rawpassword, string encodedpassword) { log.info("md5passwordencoder的matches"); return encodedpassword.equals(md5utils.encode((string)rawpassword)); } @override public string encode(charsequence rawpassword) { log.info("md5passwordencoder的encode"); return md5utils.encode((string)rawpassword); } }
5、token工具类
package com.example.security.url.utils; import cn.hutool.core.date.dateutil; import cn.hutool.core.util.strutil; import com.example.security.url.constants.resultconstant; import io.jsonwebtoken.claims; import io.jsonwebtoken.expiredjwtexception; import io.jsonwebtoken.jwts; import io.jsonwebtoken.io.decoders; import io.jsonwebtoken.security.keys; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.beans.factory.annotation.value; import org.springframework.security.core.userdetails.userdetails; import org.springframework.stereotype.component; import javax.security.sasl.authenticationexception; import java.util.date; import java.util.hashmap; import java.util.map; /** * jwttoken生成的工具类 * jwt token的格式:header.payload.signature * header的格式(算法、token的类型): * {"alg": "hs512","typ": "jwt"} * payload的格式(用户名、创建时间、生成时间): * {"sub":"wang","created":1489079981393,"exp":1489684781} * signature的生成算法: * hmacsha512(base64urlencode(header) + "." +base64urlencode(payload),secret) */ @component public class jwttokenutil { private static final logger logger = loggerfactory.getlogger(jwttokenutil.class); private static final string claim_key_username = "sub"; private static final string claim_key_created = "created"; @value("${jwt.secret}") private string secret; @value("${jwt.expiration}") private long expiration; @value("${jwt.tokentype}") private string tokentype; /** * 根据负责生成jwt的token */ private string generatetoken(map<string, object> claims) { return jwts.builder() .setclaims(claims) .setexpiration(generateexpirationdate()) .signwith(keys.hmacshakeyfor(decoders.base64.decode(secret))) .compact(); } /** * 从token中获取jwt中的负载 */ private claims getclaimsfromtoken(string token) throws authenticationexception { claims claims = null; try { claims = jwts.parserbuilder() .setsigningkey(keys.hmacshakeyfor(decoders.base64.decode(secret))) .build() .parseclaimsjws(token) .getbody(); } catch (expiredjwtexception e){ claims =e.getclaims(); } catch (exception e) { logger.error("获取token:【{}】中的jwt负载失败:【{}】", token, e.getmessage()); } return claims; } /** * 生成token的过期时间 */ private date generateexpirationdate() { return new date(system.currenttimemillis() + expiration * 1000); } /** * 从token中获取登录用户名 */ public string getusernamefromtoken(string token) { string username; try { claims claims = getclaimsfromtoken(token); username = claims.getsubject(); } catch (exception e) { username = null; } return username; } /** * 验证token中的用户是否还有效 * * @param token 客户端传入的token * @param userdetails 从数据库中查询出来的用户信息 */ public boolean validatetoken(string token, userdetails userdetails) throws authenticationexception { string username = getusernamefromtoken(token); return username.equals(userdetails.getusername()) && !istokenexpired(token); } /** * 判断token是否已经失效 */ public boolean istokenexpired(string token) throws authenticationexception { date expireddate = getexpireddatefromtoken(token); return expireddate.before(new date()); } /** * 从token中获取过期时间 */ private date getexpireddatefromtoken(string token) throws authenticationexception { claims claims = getclaimsfromtoken(token); return claims.getexpiration(); } /** * 根据用户信息生成token */ public string generatetoken(userdetails userdetails) { map<string, object> claims = new hashmap<>(); claims.put(claim_key_username, userdetails.getusername()); claims.put(claim_key_created, new date()); return generatetoken(claims); } /** * 当原来的token没过期时是可以刷新的 * * @param oldtoken 带tokenhead的token */ public string refreshheadtoken(string oldtoken) throws authenticationexception { if (strutil.isempty(oldtoken)) { return null; } string token = oldtoken.substring(tokentype.length()); if (strutil.isempty(token)) { return null; } //token校验不通过 claims claims = getclaimsfromtoken(token); if (claims == null) { return null; } //如果token已经过期,不支持刷新 if (istokenexpired(token)) { return null; } //如果token在30分钟之内刚刷新过,返回原token if (tokenrefreshjustbefore(token, 30 * 60)) { return token; } else { claims.put(claim_key_created, new date()); return generatetoken(claims); } } /** * 判断token在指定时间内是否刚刚刷新过 * * @param token 原token * @param time 指定时间(秒) */ private boolean tokenrefreshjustbefore(string token, int time) throws authenticationexception { claims claims = getclaimsfromtoken(token); date created = claims.get(claim_key_created, date.class); date refreshdate = new date(); //刷新时间在创建时间的指定时间内 if (refreshdate.after(created) && refreshdate.before(dateutil.offsetsecond(created, time))) { return true; } return false; } }
6、输入流工具类
package com.example.security.url.utils; import com.alibaba.fastjson.jsonobject; import com.alibaba.fastjson.serializer.serializerfeature; import com.example.security.url.common.result.commonresult; import javax.servlet.http.httpservletresponse; import java.io.ioexception; /** * @author deyou kong * @description 响应处理类 * @date 2023/2/9 3:33 下午 */ public class responseutils { public static void out(httpservletresponse response, commonresult result) throws ioexception { response.setcharacterencoding("utf-8"); response.setcontenttype("application/json"); response.getwriter().println(jsonobject.tojsonstring(result, serializerfeature.writemapnullvalue)); // 保留值为null的字段 response.getwriter().flush(); } }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论