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

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

今天小编给大家分享一下Springboot如何实现认证和动态权限管理的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

知识点补充

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中添加缓存配置

privatevoidenableCache(MySQLRealmrealm){//开启全局缓存配置realm.setCachingEnabled(true);//开启认证缓存配置realm.setAuthenticationCachingEnabled(true);//开启授权缓存配置realm.setAuthorizationCachingEnabled(true);//为了方便操作,我们给缓存起个名字realm.setAuthenticationCacheName("authcCache");realm.setAuthorizationCacheName("authzCache");//注入缓存实现realm.setCacheManager(newEhCacheManager());}

然后再在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需要排除,直接使用上面的shiroshiro1.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.1port:6379password:hewenpingtimeout:3000jedis:pool:min-idle:5max-active:20max-idle:15

修改ShiroConfig配置类,添加shiro-redis插件配置

/**shiro配置类*@author赖柄沣bingfengdev@aliyun.com*@version1.0*@date2020/10/69:11*/@ConfigurationpublicclassShiroConfig{privatestaticfinalStringCACHE_KEY="shiro:cache:";privatestaticfinalStringSESSION_KEY="shiro:session:";privatestaticfinalintEXPIRE=18000;@Value("${spring.redis.host}")privateStringhost;@Value("${spring.redis.port}")privateintport;@Value("${spring.redis.timeout}")privateinttimeout;@Value("${spring.redis.password}")privateStringpassword;@Value("${spring.redis.jedis.pool.min-idle}")privateintminIdle;@Value("${spring.redis.jedis.pool.max-idle}")privateintmaxIdle;@Value("${spring.redis.jedis.pool.max-active}")privateintmaxActive;@BeanpublicAuthorizationAttributeSourceAdvisorauthorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManagersecurityManager){AuthorizationAttributeSourceAdvisorauthorizationAttributeSourceAdvisor=newAuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);returnauthorizationAttributeSourceAdvisor;}/***创建ShiroFilter拦截器*@returnShiroFilterFactoryBean*/@Bean(name="shiroFilterFactoryBean")publicShiroFilterFactoryBeangetShiroFilterFactoryBean(DefaultWebSecurityManagersecurityManager){ShiroFilterFactoryBeanshiroFilterFactoryBean=newShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);//配置不拦截路径和拦截路径,顺序不能反HashMap<String,String>map=newHashMap<>(5);map.put("/authc/**","anon");map.put("/login.html","anon");map.put("/js/**","anon");map.put("/css/**","anon");map.put("/**","authc");shiroFilterFactoryBean.setFilterChainDefinitionMap(map);//覆盖默认的登录urlshiroFilterFactoryBean.setLoginUrl("/authc/unauthc");returnshiroFilterFactoryBean;}@BeanpublicRealmgetRealm(){//设置凭证匹配器,修改为hash凭证匹配器HashedCredentialsMatchermyCredentialsMatcher=newHashedCredentialsMatcher();//设置算法myCredentialsMatcher.setHashAlgorithmName("md5");//散列次数myCredentialsMatcher.setHashIterations(1024);MySQLRealmrealm=newMySQLRealm();realm.setCredentialsMatcher(myCredentialsMatcher);//开启缓存realm.setCachingEnabled(true);realm.setAuthenticationCachingEnabled(true);realm.setAuthorizationCachingEnabled(true);returnrealm;}/***创建shiroweb应用下的安全管理器*@returnDefaultWebSecurityManager*/@BeanpublicDefaultWebSecurityManagergetSecurityManager(Realmrealm){DefaultWebSecurityManagersecurityManager=newDefaultWebSecurityManager();securityManager.setRealm(realm);securityManager.setCacheManager(cacheManager());SecurityUtils.setSecurityManager(securityManager);returnsecurityManager;}/***配置Redis管理器*@Attention使用的是shiro-redis开源插件*@return*/@BeanpublicRedisManagerredisManager(){RedisManagerredisManager=newRedisManager();redisManager.setHost(host);redisManager.setPort(port);redisManager.setTimeout(timeout);redisManager.setPassword(password);JedisPoolConfigjedisPoolConfig=newJedisPoolConfig();jedisPoolConfig.setMaxTotal(maxIdle+maxActive);jedisPoolConfig.setMaxIdle(maxIdle);jedisPoolConfig.setMinIdle(minIdle);redisManager.setJedisPoolConfig(jedisPoolConfig);returnredisManager;}@BeanpublicRedisCacheManagercacheManager(){RedisCacheManagerredisCacheManager=newRedisCacheManager();redisCacheManager.setRedisManager(redisManager());redisCacheManager.setKeyPrefix(CACHE_KEY);//shiro-redis要求放在session里面的实体类必须有个id标识//这是组成redis中所存储数据的key的一部分redisCacheManager.setPrincipalIdFieldName("username");returnredisCacheManager;}}

修改MySQLRealm中的doGetAuthenticationInfo方法,将User对象整体作为SimpleAuthenticationInfo的第一个参数。shiro-redis将根据RedisCacheManagerprincipalIdFieldName属性值从第一个参数中获取id值作为redis中数据的key的一部分。

/***认证*@paramtoken*@return*@throwsAuthenticationException*/@OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokentoken)throwsAuthenticationException{if(token==null){returnnull;}Stringprincipal=(String)token.getPrincipal();Useruser=userService.findByUsername(principal);SimpleAuthenticationInfosimpleAuthenticationInfo=newMyAuthcInfo(//由于shiro-redis插件需要从这个属性中获取id作为redis的key//所有这里传的是user而不是usernameuser,//凭证信息user.getPassword(),//加密盐值newCurrentSalt(user.getSalt()),getName());returnsimpleAuthenticationInfo;}

并修改MySQLRealm中的doGetAuthorizationInfo方法,从User对象中获取主身份信息。

/***授权*@paramprincipals*@return*/@OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollectionprincipals){Useruser=(User)principals.getPrimaryPrincipal();Stringusername=user.getUsername();List<Role>roleList=roleService.findByUsername(username);SimpleAuthorizationInfoauthorizationInfo=newSimpleAuthorizationInfo();for(Rolerole:roleList){authorizationInfo.addRole(role.getRoleName());}List<Long>roleIdList=newArrayList<>();for(Rolerole:roleList){roleIdList.add(role.getRoleId());}List<Resource>resourceList=resourceService.findByRoleIds(roleIdList);for(Resourceresource:resourceList){authorizationInfo.addStringPermission(resource.getResourcePermissionTag());}returnauthorizationInfo;}

自定义Salt

由于Shiro里面默认的SimpleByteSource没有实现序列化接口,导致ByteSource.Util.bytes()生成的salt在序列化时出错,因此需要自定义Salt类并实现序列化接口。并在自定义的Realm的认证方法使用new CurrentSalt(user.getSalt())传入盐值。

/**由于shiro当中的ByteSource没有实现序列化接口,缓存时会发生错误*因此,我们需要通过自定义ByteSource的方式实现这个接口*@author赖柄沣bingfengdev@aliyun.com*@version1.0*@date2020/10/816:17*/publicclassCurrentSaltextendsSimpleByteSourceimplementsSerializable{publicCurrentSalt(Stringstring){super(string);}publicCurrentSalt(byte[]bytes){super(bytes);}publicCurrentSalt(char[]chars){super(chars);}publicCurrentSalt(ByteSourcesource){super(source);}publicCurrentSalt(Filefile){super(file);}publicCurrentSalt(InputStreamstream){super(stream);}}

添加Shiro自定义会话

添加自定义会话ID生成器

/**SessionId生成器*<p>@author赖柄沣laibingf_dev@outlook.com</p>*<p>@date2020/8/1515:19</p>*/publicclassShiroSessionIdGeneratorimplementsSessionIdGenerator{/***实现SessionId生成*@paramsession*@return*/@OverridepublicSerializablegenerateId(Sessionsession){SerializablesessionId=newJavaUuidSessionIdGenerator().generateId(session);returnString.format("login_token_%s",sessionId);}}

添加自定义会话管理器

/***<p>@author赖柄沣laibingf_dev@outlook.com</p>*<p>@date2020/8/1515:40</p>*/publicclassShiroSessionManagerextendsDefaultWebSessionManager{//定义常量privatestaticfinalStringAUTHORIZATION="Authorization";privatestaticfinalStringREFERENCED_SESSION_ID_SOURCE="Statelessrequest";//重写构造器publicShiroSessionManager(){super();this.setDeleteInvalidSessions(true);}/***重写方法实现从请求头获取Token便于接口统一**每次请求进来,*Shiro会去从请求头找Authorization这个key对应的Value(Token)*@paramrequest*@paramresponse*@return*/@OverridepublicSerializablegetSessionId(ServletRequestrequest,ServletResponseresponse){Stringtoken=WebUtils.toHttp(request).getHeader(AUTHORIZATION);//如果请求头中存在token则从请求头中获取tokenif(!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);returntoken;}else{//这里禁用掉Cookie获取方式returnnull;}}}

配置自定义会话管理器

在ShiroConfig中添加对会话管理器的配置

/***SessionID生成器**/@BeanpublicShiroSessionIdGeneratorsessionIdGenerator(){returnnewShiroSessionIdGenerator();}/***配置RedisSessionDAO*/@BeanpublicRedisSessionDAOredisSessionDAO(){RedisSessionDAOredisSessionDAO=newRedisSessionDAO();redisSessionDAO.setRedisManager(redisManager());redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());redisSessionDAO.setKeyPrefix(SESSION_KEY);redisSessionDAO.setExpire(EXPIRE);returnredisSessionDAO;}/***配置Session管理器*@AuthorSans**/@BeanpublicSessionManagersessionManager(){ShiroSessionManagershiroSessionManager=newShiroSessionManager();shiroSessionManager.setSessionDAO(redisSessionDAO());//禁用cookieshiroSessionManager.setSessionIdCookieEnabled(false);//禁用会话id重写shiroSessionManager.setSessionIdUrlRewritingEnabled(false);returnshiroSessionManager;}

目前最新版本(1.6.0)中,session管理器的setSessionIdUrlRewritingEnabled(false)配置没有生效,导致没有认证直接访问受保护资源出现多次重定向的错误。将shiro版本切换为1.5.0后就解决了这个bug。

本来这篇文章应该是昨晚发的,因为这个原因搞了好久,所有今天才发。。。

修改自定义Realm的doGetAuthenticationInfo认证方法

在认证信息返回前,我们需要做一个判断:如果当前用户已在旧设备上登录,则需要将旧设备上的会话id删掉,使其下线。

/***认证*@paramtoken*@return*@throwsAuthenticationException*/@OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokentoken)throwsAuthenticationException{if(token==null){returnnull;}Stringprincipal=(String)token.getPrincipal();Useruser=userService.findByUsername(principal);SimpleAuthenticationInfosimpleAuthenticationInfo=newMyAuthcInfo(//由于shiro-redis插件需要从这个属性中获取id作为redis的key//所有这里传的是user而不是usernameuser,//凭证信息user.getPassword(),//加密盐值newCurrentSalt(user.getSalt()),getName());//清除当前主体旧的会话,相当于你在新电脑上登录系统,把你之前在旧电脑上登录的会话挤下去ShiroUtils.deleteCache(user.getUsername(),true);returnsimpleAuthenticationInfo;}

修改login接口

我们将会话信息存储在redis中,并在用户认证通过后将会话Id以token的形式返回给用户。用户请求受保护资源时带上这个token,我们根据token信息去redis中获取用户的权限信息,从而做访问控制。

@PostMapping("/login")publicHashMap<Object,Object>login(@RequestBodyLoginVOloginVO)throwsAuthenticationException{booleanflags=authcService.login(loginVO);HashMap<Object,Object>map=newHashMap<>(3);if(flags){Serializableid=SecurityUtils.getSubject().getSession().getId();map.put("msg","登录成功");map.put("token",id);returnmap;}else{returnnull;}}

添加全局异常处理

/**shiro异常处理*@author赖柄沣bingfengdev@aliyun.com*@version1.0*@date2020/10/718:01*/@ControllerAdvice(basePackages="pers.lbf.springbootshiro")publicclassAuthExceptionHandler{//==================认证异常====================//@ExceptionHandler(ExpiredCredentialsException.class)@ResponseBodypublicStringexpiredCredentialsExceptionHandlerMethod(ExpiredCredentialsExceptione){return"凭证已过期";}@ExceptionHandler(IncorrectCredentialsException.class)@ResponseBodypublicStringincorrectCredentialsExceptionHandlerMethod(IncorrectCredentialsExceptione){return"用户名或密码错误";}@ExceptionHandler(UnknownAccountException.class)@ResponseBodypublicStringunknownAccountExceptionHandlerMethod(IncorrectCredentialsExceptione){return"用户名或密码错误";}@ExceptionHandler(LockedAccountException.class)@ResponseBodypublicStringlockedAccountExceptionHandlerMethod(IncorrectCredentialsExceptione){return"账户被锁定";}//=================授权异常=====================//@ExceptionHandler(UnauthorizedException.class)@ResponseBodypublicStringunauthorizedExceptionHandlerMethod(UnauthorizedExceptione){return"未授权!请联系管理员授权";}}

实际开发中,应该对返回结果统一化,并给出业务错误码。这已经超出了本文的范畴,如有需要,请根据自身系统特点考量。

进行测试

认证

登录成功的情况

用户名或密码错误的情况

为了安全起见,不要暴露具体是用户名错误还是密码错误。

访问受保护资源

认证后访问有权限的资源

认证后访问无权限的资源

未认证直接访问的情况

查看redis

三个键值分别对应认证信息缓存、授权信息缓存和会话信息缓存。

以上就是“Springboot如何实现认证和动态权限管理”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注恰卡编程网行业资讯频道。

发布于 2022-03-29 22:29:35
收藏
分享
海报
0 条评论
26
上一篇:SpringBoot怎么整合Shiro 下一篇:Redis的Bitmap如何使用
目录

    0 条评论

    本站已关闭游客评论,请登录或者注册后再评论吧~

    忘记密码?

    图形验证码