您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息
免费发信息

Springboot如何实现认证和动态权限管理

2024/4/19 21:52:45发布5次查看
知识点补充shiro缓存流程分析在原来的项目当中,由于没有配置缓存,因此每次需要验证当前主体有没有访问权限时,都会去查询数据库。由于权限数据是典型的读多写少的数据,因此,我们应该要对其加入缓存的支持。
当我们加入缓存后,shiro在做鉴权时先去缓存里查询相关数据,缓存里没有,则查询数据库并将查到的数据写入缓存,下次再查时就能从缓存当中获取数据,而不是从数据库中获取。这样就能改善我们的应用的性能。
接下来,我们去实现shiro的缓存管理部分。
shiro会话机制shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如 web 容器 tomcat),不管 javase 还是 javaee 环境都可以使用,提供了会话管理、会话事件监听、会话存储 / 持久化、容器无关的集群、失效 / 过期支持、对 web 的透明支持、sso 单点登录的支持等特性。
我们将使用 shiro 的会话管理来接管我们应用的web会话,并通过redis来存储会话信息。
整合步骤添加缓存cachemanager在shiro当中,它提供了cachemanager这个类来做缓存管理。
使用shiro默认的ehcache实现在shiro当中,默认使用的是ehcache缓存框架。ehcache 是一个纯java的进程内缓存框架,具有快速、精干等特点。
引入shiro-ehcache依赖<dependency> <groupid>org.apache.shiro</groupid> <artifactid>shiro-ehcache</artifactid> <version>1.4.0</version></dependency>
在springboot整合redis的过程中,还要注意版本匹配的问题,不然有可能报方法未找到的异常。
在shiroconfig中添加缓存配置private void enablecache(mysqlrealm realm){ //开启全局缓存配置 realm.setcachingenabled(true); //开启认证缓存配置 realm.setauthenticationcachingenabled(true); //开启授权缓存配置 realm.setauthorizationcachingenabled(true); //为了方便操作,我们给缓存起个名字 realm.setauthenticationcachename("authccache"); realm.setauthorizationcachename("authzcache"); //注入缓存实现 realm.setcachemanager(new ehcachemanager());}
然后再在getrealm中调用这个方法即可。
提示:在这个实现当中,只是实现了本地的缓存。也就是说缓存的数据同应用一样共用一台机器的内存。如果服务器发生宕机或意外停电,那么缓存数据也将不复存在。当然你也可通过cachemanager.setcachemanagerconfigfile()方法给予缓存更多的配置。
接下来我们将通过redis缓存我们的权限数据
使用redis实现添加依赖<!--shiro-redis相关依赖--> <dependency> <groupid>org.crazycake</groupid> <artifactid>shiro-redis</artifactid> <version>3.1.0</version> <!-- 里面这个shiro-core版本较低,会引发一个异常 classnotfoundexception: org.apache.shiro.event.eventbus 需要排除,直接使用上面的shiro shiro1.3 加入了时间总线。--> <exclusions> <exclusion> <groupid>org.apache.shiro</groupid> <artifactid>shiro-core</artifactid> </exclusion> </exclusions> </dependency>
配置redis在application.yml中添加redis的相关配置
spring: redis: host: 127.0.0.1 port: 6379 password: hewenping timeout: 3000 jedis: pool: min-idle: 5 max-active: 20 max-idle: 15
修改shiroconfig配置类,添加shiro-redis插件配置
/**shiro配置类 * @author 赖柄沣 bingfengdev@aliyun.com * @version 1.0 * @date 2020/10/6 9:11 */@configurationpublic class shiroconfig { private static final string cache_key = "shiro:cache:"; private static final string session_key = "shiro:session:"; private static final int expire = 18000; @value("${spring.redis.host}") private string host; @value("${spring.redis.port}") private int port; @value("${spring.redis.timeout}") private int timeout; @value("${spring.redis.password}") private string password; @value("${spring.redis.jedis.pool.min-idle}") private int minidle; @value("${spring.redis.jedis.pool.max-idle}") private int maxidle; @value("${spring.redis.jedis.pool.max-active}") private int maxactive; @bean public authorizationattributesourceadvisor authorizationattributesourceadvisor(org.apache.shiro.mgt.securitymanager securitymanager) { authorizationattributesourceadvisor authorizationattributesourceadvisor = new authorizationattributesourceadvisor(); authorizationattributesourceadvisor.setsecuritymanager(securitymanager); return authorizationattributesourceadvisor; } /** * 创建shirofilter拦截器 * @return shirofilterfactorybean */ @bean(name = "shirofilterfactorybean") public shirofilterfactorybean getshirofilterfactorybean(defaultwebsecuritymanager securitymanager){ shirofilterfactorybean shirofilterfactorybean = new shirofilterfactorybean(); shirofilterfactorybean.setsecuritymanager(securitymanager); //配置不拦截路径和拦截路径,顺序不能反 hashmap<string, string> map = new hashmap<>(5); map.put("/authc/**","anon"); map.put("/login.html","anon"); map.put("/js/**","anon"); map.put("/css/**","anon"); map.put("/**","authc"); shirofilterfactorybean.setfilterchaindefinitionmap(map); //覆盖默认的登录url shirofilterfactorybean.setloginurl("/authc/unauthc"); return shirofilterfactorybean; } @bean public realm getrealm(){ //设置凭证匹配器,修改为hash凭证匹配器 hashedcredentialsmatcher mycredentialsmatcher = new hashedcredentialsmatcher(); //设置算法 mycredentialsmatcher.sethashalgorithmname("md5"); //散列次数 mycredentialsmatcher.sethashiterations(1024); mysqlrealm realm = new mysqlrealm(); realm.setcredentialsmatcher(mycredentialsmatcher); //开启缓存 realm.setcachingenabled(true); realm.setauthenticationcachingenabled(true); realm.setauthorizationcachingenabled(true); return realm; } /** * 创建shiro web应用下的安全管理器 * @return defaultwebsecuritymanager */ @bean public defaultwebsecuritymanager getsecuritymanager( realm realm){ defaultwebsecuritymanager securitymanager = new defaultwebsecuritymanager(); securitymanager.setrealm(realm); securitymanager.setcachemanager(cachemanager()); securityutils.setsecuritymanager(securitymanager); return securitymanager; } /** * 配置redis管理器 * @attention 使用的是shiro-redis开源插件 * @return */ @bean public redismanager redismanager() { redismanager redismanager = new redismanager(); redismanager.sethost(host); redismanager.setport(port); redismanager.settimeout(timeout); redismanager.setpassword(password); jedispoolconfig jedispoolconfig = new jedispoolconfig(); jedispoolconfig.setmaxtotal(maxidle+maxactive); jedispoolconfig.setmaxidle(maxidle); jedispoolconfig.setminidle(minidle); redismanager.setjedispoolconfig(jedispoolconfig); return redismanager; } @bean public rediscachemanager cachemanager() { rediscachemanager rediscachemanager = new rediscachemanager(); rediscachemanager.setredismanager(redismanager()); rediscachemanager.setkeyprefix(cache_key); // shiro-redis要求放在session里面的实体类必须有个id标识 //这是组成redis中所存储数据的key的一部分 rediscachemanager.setprincipalidfieldname("username"); return rediscachemanager; }}
修改mysqlrealm中的dogetauthenticationinfo方法,将user对象整体作为simpleauthenticationinfo的第一个参数。shiro-redis将根据rediscachemanager的principalidfieldname属性值从第一个参数中获取id值作为redis中数据的key的一部分。
/** * 认证 * @param token * @return * @throws authenticationexception */@overrideprotected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception { if(token==null){ return null; } string principal = (string) token.getprincipal(); user user = userservice.findbyusername(principal); simpleauthenticationinfo simpleauthenticationinfo = new myauthcinfo( //由于shiro-redis插件需要从这个属性中获取id作为redis的key //所有这里传的是user而不是username user, //凭证信息 user.getpassword(), //加密盐值 new currentsalt(user.getsalt()), getname()); return simpleauthenticationinfo;}
并修改mysqlrealm中的dogetauthorizationinfo方法,从user对象中获取主身份信息。
/** * 授权 * @param principals * @return */@overrideprotected authorizationinfo dogetauthorizationinfo(principalcollection principals) { user user = (user) principals.getprimaryprincipal(); string username = user.getusername(); list<role> rolelist = roleservice.findbyusername(username); simpleauthorizationinfo authorizationinfo = new simpleauthorizationinfo(); for (role role : rolelist) { authorizationinfo.addrole(role.getrolename()); } list<long> roleidlist = new arraylist<>(); for (role role : rolelist) { roleidlist.add(role.getroleid()); } list<resource> resourcelist = resourceservice.findbyroleids(roleidlist); for (resource resource : resourcelist) { authorizationinfo.addstringpermission(resource.getresourcepermissiontag()); } return authorizationinfo;}
自定义salt由于shiro里面默认的simplebytesource没有实现序列化接口,导致bytesource.util.bytes()生成的salt在序列化时出错,因此需要自定义salt类并实现序列化接口。并在自定义的realm的认证方法使用new currentsalt(user.getsalt())传入盐值。
/**由于shiro当中的bytesource没有实现序列化接口,缓存时会发生错误 * 因此,我们需要通过自定义bytesource的方式实现这个接口 * @author 赖柄沣 bingfengdev@aliyun.com * @version 1.0 * @date 2020/10/8 16:17 */public class currentsalt extends simplebytesource implements serializable { public currentsalt(string string) { super(string); } public currentsalt(byte[] bytes) { super(bytes); } public currentsalt(char[] chars) { super(chars); } public currentsalt(bytesource source) { super(source); } public currentsalt(file file) { super(file); } public currentsalt(inputstream stream) { super(stream); }}
添加shiro自定义会话添加自定义会话id生成器/**sessionid生成器 * <p>@author 赖柄沣 laibingf_dev@outlook.com</p> * <p>@date 2020/8/15 15:19</p> */public class shirosessionidgenerator implements sessionidgenerator { /** *实现sessionid生成 * @param session * @return */ @override public serializable generateid(session session) { serializable sessionid = new javauuidsessionidgenerator().generateid(session); return string.format("login_token_%s", sessionid); }}
添加自定义会话管理器/** * <p>@author 赖柄沣 laibingf_dev@outlook.com</p> * <p>@date 2020/8/15 15:40</p> */public class shirosessionmanager extends defaultwebsessionmanager { //定义常量 private static final string authorization = "authorization"; private static final string referenced_session_id_source = "stateless request"; //重写构造器 public shirosessionmanager() { super(); this.setdeleteinvalidsessions(true); } /** * 重写方法实现从请求头获取token便于接口统一 * * 每次请求进来, * shiro会去从请求头找authorization这个key对应的value(token) * @param request * @param response * @return */ @override public serializable getsessionid(servletrequest request, servletresponse response) { string token = webutils.tohttp(request).getheader(authorization); //如果请求头中存在token 则从请求头中获取token if (!stringutils.isempty(token)) { request.setattribute(shirohttpservletrequest.referenced_session_id_source, referenced_session_id_source); request.setattribute(shirohttpservletrequest.referenced_session_id, token); request.setattribute(shirohttpservletrequest.referenced_session_id_is_valid, boolean.true); return token; } else { // 这里禁用掉cookie获取方式 return null; } }}
配置自定义会话管理器在shiroconfig中添加对会话管理器的配置
/** * sessionid生成器 * */@beanpublic shirosessionidgenerator sessionidgenerator(){ return new shirosessionidgenerator();}/** * 配置redissessiondao */@beanpublic redissessiondao redissessiondao() { redissessiondao redissessiondao = new redissessiondao(); redissessiondao.setredismanager(redismanager()); redissessiondao.setsessionidgenerator(sessionidgenerator()); redissessiondao.setkeyprefix(session_key); redissessiondao.setexpire(expire); return redissessiondao;}/** * 配置session管理器 * @author sans * */@beanpublic sessionmanager sessionmanager() { shirosessionmanager shirosessionmanager = new shirosessionmanager(); shirosessionmanager.setsessiondao(redissessiondao()); //禁用cookie shirosessionmanager.setsessionidcookieenabled(false); //禁用会话id重写 shirosessionmanager.setsessionidurlrewritingenabled(false); return shirosessionmanager;}
目前最新版本(1.6.0)中,session管理器的setsessionidurlrewritingenabled(false)配置没有生效,导致没有认证直接访问受保护资源出现多次重定向的错误。将shiro版本切换为1.5.0后就解决了这个bug。
本来这篇文章应该是昨晚发的,因为这个原因搞了好久,所有今天才发。。。
修改自定义realm的dogetauthenticationinfo认证方法在认证信息返回前,我们需要做一个判断:如果当前用户已在旧设备上登录,则需要将旧设备上的会话id删掉,使其下线。
/** * 认证 * @param token * @return * @throws authenticationexception */@overrideprotected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception { if(token==null){ return null; } string principal = (string) token.getprincipal(); user user = userservice.findbyusername(principal); simpleauthenticationinfo simpleauthenticationinfo = new myauthcinfo( //由于shiro-redis插件需要从这个属性中获取id作为redis的key //所有这里传的是user而不是username user, //凭证信息 user.getpassword(), //加密盐值 new currentsalt(user.getsalt()), getname()); //清除当前主体旧的会话,相当于你在新电脑上登录系统,把你之前在旧电脑上登录的会话挤下去 shiroutils.deletecache(user.getusername(),true); return simpleauthenticationinfo;}
修改login接口我们将会话信息存储在redis中,并在用户认证通过后将会话id以token的形式返回给用户。用户请求受保护资源时带上这个token,我们根据token信息去redis中获取用户的权限信息,从而做访问控制。
@postmapping("/login")public hashmap<object, object> login(@requestbody loginvo loginvo) throws authenticationexception { boolean flags = authcservice.login(loginvo); hashmap<object, object> map = new hashmap<>(3); if (flags){ serializable id = securityutils.getsubject().getsession().getid(); map.put("msg","登录成功"); map.put("token",id); return map; }else { return null; } }
添加全局异常处理/**shiro异常处理 * @author 赖柄沣 bingfengdev@aliyun.com * @version 1.0 * @date 2020/10/7 18:01 */@controlleradvice(basepackages = "pers.lbf.springbootshiro")public class authexceptionhandler { //==================认证异常====================// @exceptionhandler(expiredcredentialsexception.class) @responsebody public string expiredcredentialsexceptionhandlermethod(expiredcredentialsexception e) { return "凭证已过期"; } @exceptionhandler(incorrectcredentialsexception.class) @responsebody public string incorrectcredentialsexceptionhandlermethod(incorrectcredentialsexception e) { return "用户名或密码错误"; } @exceptionhandler(unknownaccountexception.class) @responsebody public string unknownaccountexceptionhandlermethod(incorrectcredentialsexception e) { return "用户名或密码错误"; } @exceptionhandler(lockedaccountexception.class) @responsebody public string lockedaccountexceptionhandlermethod(incorrectcredentialsexception e) { return "账户被锁定"; } //=================授权异常=====================// @exceptionhandler(unauthorizedexception.class) @responsebody public string unauthorizedexceptionhandlermethod(unauthorizedexception e){ return "未授权!请联系管理员授权"; }}
实际开发中,应该对返回结果统一化,并给出业务错误码。这已经超出了本文的范畴,如有需要,请根据自身系统特点考量。
进行测试认证登录成功的情况
用户名或密码错误的情况
为了安全起见,不要暴露具体是用户名错误还是密码错误。
访问受保护资源认证后访问有权限的资源
认证后访问无权限的资源
未认证直接访问的情况
查看redis
三个键值分别对应认证信息缓存、授权信息缓存和会话信息缓存。
以上就是springboot如何实现认证和动态权限管理的详细内容。
该用户其它信息

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录