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
    @Component
    public class ShiroRealm extends AuthorizingRealm {
          
          
    
       @Autowired
       private IUserService userService;
    
       @Autowired
       private StringRedisTemplate stringRedisTemplate;
    
       /**
        * 必须重写此方法,不然Shiro会报错
        */
       @Override
       public boolean supports(AuthenticationToken token) {
          
          
           return token instanceof JwtToken;
       }
    
       /**
        * 授权(验证权限时调用)
        */
       @Override
       protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
          
          
           SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
           return info;
       }
    
       /**
        * 认证(登录时调用)
        */
       @Override
       protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
          
          
           String accessToken = (String) token.getPrincipal();
           if (accessToken == null) {
          
          
               throw new AuthenticationException(CommonCode.WEB_TOKEN_NULL.getMessage());
           }
           // 校验token有效性
           User tokenEntity = this.checkUserTokenIsEffect(accessToken);
           SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(tokenEntity, accessToken, getName());
           return info;
       }
    
       /**
        * 校验token的有效性
        *  springboot2.3.+新增了一个配置项server.error.includeMessage,默认是NEVER,
        *  因此默认是不是输出message的,只要开启就可以了,否则无法拿到shiro抛出异常信息message
        * @param token
        */
       public User checkUserTokenIsEffect(String token) throws AuthenticationException {
          
          
           // 解密获得username,用于和数据库进行对比
           String userId = JwtUtil.getUserId(token);
           if (userId == null) {
          
          
               throw new AuthenticationException(CommonCode.WEB_TOKEN_ILLEGAL.getMessage());
           }
    
           // 查询用户信息
           User loginUser = userService.getById(userId);
           if (loginUser == null) {
          
          
               throw new AuthenticationException(CommonCode.WEB_USER_NOT_EXIST.getMessage());
           }
           // 判断用户状态
           if (loginUser.getStatus() != 0) {
          
          
               throw new LockedAccountException(CommonCode.WEB_ACCOUNT_LOCKED.getMessage());
           }
           // 校验token是否超时失效 & 或者账号密码是否错误
           if (!jwtTokenRefresh(token, userId, loginUser.getUserPhone())) {
          
          
               throw new IncorrectCredentialsException(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
        */
       public boolean jwtTokenRefresh(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);
               }
               return true;
           }
           return false;
       }
    
       /**
        * 清除当前用户的权限认证缓存
        *
        * @param principals 权限信息
        */
       @Override
       public void clearCache(PrincipalCollection principals) {
          
          
           super.clearCache(principals);
       }
    }
    
  • JwtFilter
    这里会拦截需要认证和鉴权的请求,同时会捕获相应异常并抛出

    /**
     * 过滤器
     *
     * @author zwj
     */
    public class JwtFilter extends BasicHttpAuthenticationFilter {
          
          
    
        /**
         * 功能描述: 执行登录认证
         *
         * @param request
         * @param response
         * @param mappedValue
         * @return boolean
         * @author zhouwenjie
         * @date 2021/12/24 14:45
         */
        @SneakyThrows
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
          
          
            try {
          
          
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
          
          
                throw new AuthenticationException(e.getMessage(), e);
            }
        }
    
        @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response) {
          
          
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String token = JwtUtil.getTokenByRequest(httpServletRequest);
            JwtToken jwtToken = new JwtToken(token);
            // 提交给realm进行登入,如果错误他会抛出异常并被捕获
            getSubject(request, response).login(jwtToken);
            // 如果没有抛出异常则代表登入成功,返回true
            return true;
        }
    
        /**
         * 对跨域提供支持
         */
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
          
          
            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());
                return false;
            }
            return super.preHandle(request, response);
        }
    
    }
    

    JwtToken

    /**
     * token
     *
     * @author Mark [email protected]
     */
    public class JwtToken implements AuthenticationToken {
          
          
        private static final long serialVersionUID = 1L;
        private String token;
    
        public JwtToken(String token){
          
          
            this.token = token;
        }
    
        @Override
        public String getPrincipal() {
          
          
            return token;
        }
    
        @Override
        public Object getCredentials() {
          
          
            return token;
        }
    }
    

    JwtUtil:token工具类

    
    /**
     * @Author zwj
     * @Desc JWT工具类
     **/
    public class JwtUtil {
          
          
    
        // Token过期时间180天(用户登录过期时间是此时间的两倍,以token在reids缓存时间为准)
        public static final long EXPIRE_TIME = 24 * 180 * 60 * 60 * 1000;
        public static final int days = 360;
        private static StringRedisTemplate stringRedisTemplate = SpringContextUtils.getBean(StringRedisTemplate.class);
    
        /**
         * 校验token是否正确
         *
         * @param token     密钥
         * @param userPhone 用户的密码
         * @return 是否正确
         */
        public static boolean verify(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);
                return true;
            } catch (Exception exception) {
          
          
                return false;
            }
        }
    
        /**
         * 获得token中的信息无需secret解密也能获得
         *
         * @return token中包含的用户名
         */
        public static String getUserId(String token) {
          
          
            try {
          
          
                DecodedJWT jwt = JWT.decode(token);
                return jwt.getClaim("userId").asString();
            } catch (Exception e) {
          
          
                return null;
            }
        }
    
        /**
         * 生成签名,360天后过期
         *
         * @param userId    用户id
         * @param userPhone 用户的密码
         * @return 加密的token
         */
        public static String sign(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
         */
        public static String getUserIdByToken(HttpServletRequest request) {
          
          
            String accessToken = getTokenByRequest(request);
            String userId = getUserId(accessToken);
            return userId;
        }
    
        /**
         * 获取 request 里传递的 token
         *
         * @param request
         * @return
         */
        public static String getTokenByRequest(HttpServletRequest request) {
          
          
            String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
            return token;
        }
    }
    

    过期时间根据自己需求设定。

  • ShiroConfig
    整合各项配置的联系,注意新版本和老版本的配置区别,新版本需要重新注入beanDefaultAdvisorAutoProxyCreator、AuthorizationAttributeSourceAdvisor,原因代码中也有详细注释。

    /**
     * Shiro配置
     *
     * @author zwj
     */
    @Configuration
    public class ShiroConfig {
          
          
    
        @Bean("shiroFilter")
        public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
          
          
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager);
    
    
            Map<String, String> filterMap = new LinkedHashMap<>();
            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 = new HashMap<>();
            filters.put("jwt", new JwtFilter());
            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")
        public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm,RedisProperties redisProperties) {
          
          
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(shiroRealm);
            //关闭shiro自带的session存放token功能
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);
            //使用redis设置自定义缓存token
            securityManager.setCacheManager(redisCacheManager(redisProperties));
            return securityManager;
        }
    
        /**
         * cacheManager 缓存 redis实现
         * 使用的是shiro-redis开源插件
         *
         * @return
         */
        public RedisCacheManager redisCacheManager(RedisProperties redisProperties) {
          
          
            RedisCacheManager redisCacheManager = new RedisCacheManager();
            redisCacheManager.setRedisManager(redisManager(redisProperties));
            //redis中针对不同用户缓存(此处的id需要对应user实体中的userId字段,用于唯一标识)
            redisCacheManager.setPrincipalIdFieldName("id");
            //用户权限信息缓存时间
            redisCacheManager.setExpire(200000);
            return redisCacheManager;
        }
    
        /**
         * 配置shiro redisManager
         * 使用的是shiro-redis开源插件
         *
         * @return
         */
        @Bean
        public RedisManager redisManager(RedisProperties redisProperties) {
          
          
            RedisManager redisManager = new RedisManager();
            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")
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
          
          
            return new LifecycleBeanPostProcessor();
        }
    
        /**
         *功能描述: 高版本shrio增加配置,否则类里方法上有@RequiresPermissions注解的,会导致整个类下的接口无法访问404
         * @author zhouwenjie
         * @date 2021/12/29 9:08
         * @param
         * @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
         */
        @Bean
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
          
          
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
          
          
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(securityManager);
            return advisor;
        }
    
    }
    

运用

/**
	 * 保存用户
	 */
	@ApiOperation(value = "保存用户", notes = "保存用户")
	@SysLog("保存用户")
	@PostMapping("/save")
	@RequiresPermissions("sys:user:save")
	public Result save(@RequestBody SysUserEntity user){
    
    
		ValidatorUtils.validateEntity(user, ValidGroups.AddGroup.class);
		
		user.setCreateUserId(getUserId());
		sysUserService.saveUser(user);
		
		return Result.ok();
	}

猜你喜欢

转载自blog.csdn.net/zwjzone/article/details/125042323