0


springboot整合shiro+jwt+redis详解

原理

三大核心组件:Subject、SecurityManager、Realm

在这里插入图片描述

  • Subject 主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;
  • SecurityManager 安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;
  • Realm 域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。 > 总结:> 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;> 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。

内部架构图:
在这里插入图片描述

整合

在springboot中整合shiro、redis和jwt,核心的配置:

ShiroConfig、JwtFilter、ShiroRealm

,其中jwt主要是负责生成token的工具,redis负责缓存token。
首先我们配置Realm,然后配置filter及jwt工具类,再用shiroConfig来将这些配置联系起来,组成完整的认证鉴权系统。

准备工作

springboot版本

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.5.RELEASE</version><relativePath/></parent>

jwt和shiro版本

<!--JWT--><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.11.0</version></dependency><!--shiro--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-starter</artifactId><version>1.7.1</version></dependency><dependency><groupId>org.crazycake</groupId><artifactId>shiro-redis</artifactId><version>3.1.0</version><exclusions><exclusion><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId></exclusion><exclusion><groupId>com.puppycrawl.tools</groupId><artifactId>checkstyle</artifactId></exclusion></exclusions></dependency>

各项配置

  • ShiroRealm 主要负责认证(AuthenticationInfo)和鉴权(AuthorizationInfo)代码逻辑的实现。/*** 认证** @author zwj*/@Slf4j@ComponentpublicclassShiroRealmextendsAuthorizingRealm{@AutowiredprivateIUserService userService;@AutowiredprivateStringRedisTemplate stringRedisTemplate;/** * 必须重写此方法,不然Shiro会报错 */@Overridepublicbooleansupports(AuthenticationToken token){return token instanceofJwtToken;}/** * 授权(验证权限时调用) */@OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollection principals){SimpleAuthorizationInfo info =newSimpleAuthorizationInfo();return info;}/** * 认证(登录时调用) */@OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationToken token)throwsAuthenticationException{String accessToken =(String) token.getPrincipal();if(accessToken ==null){thrownewAuthenticationException(CommonCode.WEB_TOKEN_NULL.getMessage());}// 校验token有效性User tokenEntity =this.checkUserTokenIsEffect(accessToken);SimpleAuthenticationInfo info =newSimpleAuthenticationInfo(tokenEntity, accessToken,getName());return info;}/** * 校验token的有效性 * springboot2.3.+新增了一个配置项server.error.includeMessage,默认是NEVER, * 因此默认是不是输出message的,只要开启就可以了,否则无法拿到shiro抛出异常信息message * @param token */publicUsercheckUserTokenIsEffect(String token)throwsAuthenticationException{// 解密获得username,用于和数据库进行对比String userId =JwtUtil.getUserId(token);if(userId ==null){thrownewAuthenticationException(CommonCode.WEB_TOKEN_ILLEGAL.getMessage());}// 查询用户信息User loginUser = userService.getById(userId);if(loginUser ==null){thrownewAuthenticationException(CommonCode.WEB_USER_NOT_EXIST.getMessage());}// 判断用户状态if(loginUser.getStatus()!=0){thrownewLockedAccountException(CommonCode.WEB_ACCOUNT_LOCKED.getMessage());}// 校验token是否超时失效 & 或者账号密码是否错误if(!jwtTokenRefresh(token, userId, loginUser.getUserPhone())){thrownewIncorrectCredentialsException(CommonCode.WEB_TOKEN_FAILURE.getMessage());}return loginUser;}/** * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能) * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍 * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证 * 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算 * 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。 * 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。 * 用户过期时间 = Jwt有效时间 * 2。 * * @param userId * @param userPhone * @return */publicbooleanjwtTokenRefresh(String token,String userId,String userPhone){//如果缓存中的token为空,直接返回失效异常String cacheToken = stringRedisTemplate.opsForValue().get(CommonConstant.PREFIX_USER_TOKEN + token);if(!StrUtils.isBlank(cacheToken)){// 校验token有效性if(!JwtUtil.verify(cacheToken, userId, userPhone)){JwtUtil.sign(userId, userPhone);}returntrue;}returnfalse;}/** * 清除当前用户的权限认证缓存 * * @param principals 权限信息 */@OverridepublicvoidclearCache(PrincipalCollection principals){super.clearCache(principals);}}
  • JwtFilter 这里会拦截需要认证和鉴权的请求,同时会捕获相应异常并抛出/** * 过滤器 * * @author zwj */publicclassJwtFilterextendsBasicHttpAuthenticationFilter{/** * 功能描述: 执行登录认证 * * @param request * @param response * @param mappedValue * @return boolean * @author zhouwenjie * @date 2021/12/24 14:45 */@SneakyThrows@OverrideprotectedbooleanisAccessAllowed(ServletRequest request,ServletResponse response,Object mappedValue){try{executeLogin(request, response);returntrue;}catch(Exception e){thrownewAuthenticationException(e.getMessage(), e);}}@OverrideprotectedbooleanexecuteLogin(ServletRequest request,ServletResponse response){HttpServletRequest httpServletRequest =(HttpServletRequest) request;String token =JwtUtil.getTokenByRequest(httpServletRequest);JwtToken jwtToken =newJwtToken(token);// 提交给realm进行登入,如果错误他会抛出异常并被捕获getSubject(request, response).login(jwtToken);// 如果没有抛出异常则代表登入成功,返回truereturntrue;}/** * 对跨域提供支持 */@OverrideprotectedbooleanpreHandle(ServletRequest request,ServletResponse response)throwsException{HttpServletRequest httpServletRequest =(HttpServletRequest) request;HttpServletResponse httpServletResponse =(HttpServletResponse) response; httpServletResponse.setHeader("Access-Control-Allow-Credentials","true"); httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods","GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态if(httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())){ httpServletResponse.setStatus(HttpStatus.OK.value());returnfalse;}returnsuper.preHandle(request, response);}}JwtToken/** * token * * @author Mark [email protected] */publicclassJwtTokenimplementsAuthenticationToken{privatestaticfinallong serialVersionUID =1L;privateString token;publicJwtToken(String token){this.token = token;}@OverridepublicStringgetPrincipal(){return token;}@OverridepublicObjectgetCredentials(){return token;}}JwtUtil:token工具类/** * @Author zwj * @Desc JWT工具类 **/publicclassJwtUtil{// Token过期时间180天(用户登录过期时间是此时间的两倍,以token在reids缓存时间为准)publicstaticfinallong EXPIRE_TIME =24*180*60*60*1000;publicstaticfinalint days =360;privatestaticStringRedisTemplate stringRedisTemplate =SpringContextUtils.getBean(StringRedisTemplate.class);/** * 校验token是否正确 * * @param token 密钥 * @param userPhone 用户的密码 * @return 是否正确 */publicstaticbooleanverify(String token,String userId,String userPhone){try{// 根据密码生成JWT效验器Algorithm algorithm =Algorithm.HMAC256(userPhone);JWTVerifier verifier = JWT.require(algorithm).withClaim("userId", userId).build();// 效验TOKEN verifier.verify(token);returntrue;}catch(Exception exception){returnfalse;}}/** * 获得token中的信息无需secret解密也能获得 * * @return token中包含的用户名 */publicstaticStringgetUserId(String token){try{DecodedJWT jwt = JWT.decode(token);return jwt.getClaim("userId").asString();}catch(Exception e){returnnull;}}/** * 生成签名,360天后过期 * * @param userId 用户id * @param userPhone 用户的密码 * @return 加密的token */publicstaticStringsign(String userId,String userPhone){// Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);Algorithm algorithm =Algorithm.HMAC256(userPhone);// 附带userId信息 可以将user信息转成map存到这里// String token = JWT.create().withClaim("userId", userId).withExpiresAt(date).sign(algorithm);String token = JWT.create().withClaim("userId", userId).sign(algorithm); stringRedisTemplate.opsForValue().set(CommonConstant.PREFIX_USER_TOKEN + token, token, days,TimeUnit.DAYS);return token;}/** * 根据request中的token获取用户账号 * * @param request * @return */publicstaticStringgetUserIdByToken(HttpServletRequest request){String accessToken =getTokenByRequest(request);String userId =getUserId(accessToken);return userId;}/** * 获取 request 里传递的 token * * @param request * @return */publicstaticStringgetTokenByRequest(HttpServletRequest request){String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);return token;}}过期时间根据自己需求设定。
  • ShiroConfig 整合各项配置的联系,注意新版本和老版本的配置区别,新版本需要重新注入beanDefaultAdvisorAutoProxyCreator、AuthorizationAttributeSourceAdvisor,原因代码中也有详细注释。/** * Shiro配置 * * @author zwj */@ConfigurationpublicclassShiroConfig{@Bean("shiroFilter")publicShiroFilterFactoryBeanshiroFilter(SecurityManager securityManager){ShiroFilterFactoryBean shiroFilter =newShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager);Map<String,String> filterMap =newLinkedHashMap<>(); filterMap.put("/web/login/**","anon"); filterMap.put("/web/carOwner/list","anon"); filterMap.put("/web/passengers/list","anon"); filterMap.put("/web/user/sendSms","anon"); filterMap.put("/web/sysDictionary/queryByIds","anon"); filterMap.put("/web/user/addActive","anon"); filterMap.put("/web/sysNotice/list","anon"); filterMap.put("/web/sysAds/addViewNum","anon"); filterMap.put("/web/sysAds/list","anon"); filterMap.put("/web/sysAds/queryById","anon"); filterMap.put("/web/sysArea/list","anon");//-------防止api文档被过滤掉 filterMap.put("/doc.html","anon"); filterMap.put("/**/*.js","anon"); filterMap.put("/**/*.css","anon"); filterMap.put("/**/*.html","anon"); filterMap.put("/**/*.svg","anon"); filterMap.put("/**/*.pdf","anon"); filterMap.put("/**/*.jpg","anon"); filterMap.put("/**/*.png","anon"); filterMap.put("/**/*.ico","anon"); filterMap.put("/swagger-resources/**","anon"); filterMap.put("/v2/api-docs","anon"); filterMap.put("/v2/api-docs-ext","anon"); filterMap.put("/webjars/**","anon"); filterMap.put("/druid/**","anon"); filterMap.put("/","anon");//=======防止api文档被过滤掉 filterMap.put("/**","jwt");//jwt过滤Map<String,Filter> filters =newHashMap<>(); filters.put("jwt",newJwtFilter()); shiroFilter.setFilters(filters); shiroFilter.setFilterChainDefinitionMap(filterMap);return shiroFilter;}/** * 功能描述: 注入realm进行安全管理 * * @param shiroRealm * @return org.apache.shiro.web.mgt.DefaultWebSecurityManager * @author zhouwenjie * @date 2021/5/5 23:09 */@Bean("securityManager")publicDefaultWebSecurityManagersecurityManager(ShiroRealm shiroRealm,RedisProperties redisProperties){DefaultWebSecurityManager securityManager =newDefaultWebSecurityManager(); securityManager.setRealm(shiroRealm);//关闭shiro自带的session存放token功能DefaultSubjectDAO subjectDAO =newDefaultSubjectDAO();DefaultSessionStorageEvaluator defaultSessionStorageEvaluator =newDefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO);//使用redis设置自定义缓存token securityManager.setCacheManager(redisCacheManager(redisProperties));return securityManager;}/** * cacheManager 缓存 redis实现 * 使用的是shiro-redis开源插件 * * @return */publicRedisCacheManagerredisCacheManager(RedisProperties redisProperties){RedisCacheManager redisCacheManager =newRedisCacheManager(); redisCacheManager.setRedisManager(redisManager(redisProperties));//redis中针对不同用户缓存(此处的id需要对应user实体中的userId字段,用于唯一标识) redisCacheManager.setPrincipalIdFieldName("id");//用户权限信息缓存时间 redisCacheManager.setExpire(200000);return redisCacheManager;}/** * 配置shiro redisManager * 使用的是shiro-redis开源插件 * * @return */@BeanpublicRedisManagerredisManager(RedisProperties redisProperties){RedisManager redisManager =newRedisManager(); redisManager.setHost(redisProperties.getHost()); redisManager.setPort(redisProperties.getPort()); redisManager.setTimeout(0);if(!StringUtils.isEmpty(redisProperties.getPassword())){ redisManager.setPassword(redisProperties.getPassword());}return redisManager;/*IRedisManager manager; // redis 单机支持,在集群为空,或者集群无机器时候使用 add by [email protected] if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) { RedisManager redisManager = new RedisManager(); redisManager.setHost(lettuceConnectionFactory.getHostName()); redisManager.setPort(lettuceConnectionFactory.getPort()); redisManager.setTimeout(0); if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) { redisManager.setPassword(lettuceConnectionFactory.getPassword()); } manager = redisManager; }else{ // redis 集群支持,优先使用集群配置 add by [email protected] RedisClusterManager redisManager = new RedisClusterManager(); Set<HostAndPort> portSet = new HashSet<>(); lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort()))); JedisCluster jedisCluster = new JedisCluster(portSet); redisManager.setJedisCluster(jedisCluster); manager = redisManager; } return manager;*/}@Bean("lifecycleBeanPostProcessor")publicLifecycleBeanPostProcessorlifecycleBeanPostProcessor(){returnnewLifecycleBeanPostProcessor();}/** *功能描述: 高版本shrio增加配置,否则类里方法上有@RequiresPermissions注解的,会导致整个类下的接口无法访问404 * @author zhouwenjie * @date 2021/12/29 9:08 * @param * @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator */@BeanpublicDefaultAdvisorAutoProxyCreatoradvisorAutoProxyCreator(){DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator =newDefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true);return advisorAutoProxyCreator;}@BeanpublicAuthorizationAttributeSourceAdvisorauthorizationAttributeSourceAdvisor(SecurityManager securityManager){AuthorizationAttributeSourceAdvisor advisor =newAuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager);return advisor;}}

运用

/**
     * 保存用户
     */@ApiOperation(value ="保存用户", notes ="保存用户")@SysLog("保存用户")@PostMapping("/save")@RequiresPermissions("sys:user:save")publicResultsave(@RequestBodySysUserEntity user){ValidatorUtils.validateEntity(user,ValidGroups.AddGroup.class);
        
        user.setCreateUserId(getUserId());
        sysUserService.saveUser(user);returnResult.ok();}
标签: redis spring boot java

本文转载自: https://blog.csdn.net/zwjzone/article/details/125042323
版权归原作者 码里法 所有, 如有侵权,请联系我们删除。

“springboot整合shiro+jwt+redis详解”的评论:

还没有评论