zoukankan      html  css  js  c++  java
  • 03 整合shiro+jwt 会话共享

    整合shiro+jwt(Json Web Token),并会话共享

    考虑到后面可能需要做集群、负载均衡等,所以就需要会话共享,而shiro的缓存和会话信息,我们一般考虑使用redis来存储这些数据,所以,我们不仅仅需要整合shiro,同时也需要整合redis。在开源的项目中,我们找到了一个starter可以快速整合shiro-redis,配置简单,这里也推荐大家使用。

    而因为我们需要做的是前后端分离项目的骨架,所以一般我们会采用token或者jwt作为跨域身份验证解决方案。所以整合shiro的过程中,我们需要引入jwt的身份验证过程。

    那么我们就开始整合:

    我们使用一个shiro-redis-spring-boot-starter的jar包,具体教程可以看官方文档:github.com/alexxiyang/…

    第一步:

    • 导入shiro-redis的starter包:还有jwt的工具包,以及为了简化开发,我引入了hutool工具包。
    <dependency>
        <groupId>org.crazycake</groupId>
        <artifactId>shiro-redis-spring-boot-starter</artifactId>
        <version>3.2.1</version>
    </dependency>
    <!-- hutool工具类-->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.3.3</version>
    </dependency>
    <!-- jwt -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    

    第二步:

    • 编写配置:

    ShiroConfig

    • com.gychen.config.ShiroConfig
    /**
     * shiro启用注解拦截控制器
     */
    @Configuration
    public class ShiroConfig {
        @Autowired
        JwtFilter jwtFilter;
        @Bean
        public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
            DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
            sessionManager.setSessionDAO(redisSessionDAO);
            return sessionManager;
        }
        @Bean
        public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
                                                         SessionManager sessionManager,
                                                         RedisCacheManager redisCacheManager) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
            securityManager.setSessionManager(sessionManager);
            securityManager.setCacheManager(redisCacheManager);
            /*
             * 关闭shiro自带的session,详情见文档
             */
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);
            return securityManager;
        }
        @Bean
        public ShiroFilterChainDefinition shiroFilterChainDefinition() {
            DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
            Map<String, String> filterMap = new LinkedHashMap<>();
            filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限
            chainDefinition.addPathDefinitions(filterMap);
            return chainDefinition;
        }
        @Bean("shiroFilterFactoryBean")
        public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                             ShiroFilterChainDefinition shiroFilterChainDefinition) {
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager);
            Map<String, Filter> filters = new HashMap<>();
            filters.put("jwt", jwtFilter);
            shiroFilter.setFilters(filters);
            Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
            shiroFilter.setFilterChainDefinitionMap(filterMap);
            return shiroFilter;
        }
    
        // 开启注解代理(默认好像已经开启,可以不要)
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
        @Bean
        public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
            return creator;
        }
    }
    
    

    上面ShiroConfig,我们主要做了几件事情:

    1. 引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。

    2. 重写了SessionManager和DefaultWebSecurityManager,同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,我们需要设置为false,这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。

    3. 在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径,而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication,这样控制权限访问。

    4. 启动如果报错404 redis相关,那就是需要在本地下载redis并配置后打开,

    那么,接下来,我们聊聊ShiroConfig中出现的AccountRealm,还有JwtFilter。

    AccountRealm

    AccountRealm是shiro进行登录或者权限校验的逻辑所在,算是核心了,我们需要重写3个方法,分别是

    • supports:为了让realm支持jwt的凭证校验
    • doGetAuthorizationInfo:权限校验
    • doGetAuthenticationInfo:登录认证校验

    我们先来总体看看AccountRealm的代码,然后逐个分析:

    • com.gychen.shiro.AccountRealm
    @Slf4j
    @Component
    public class AccountRealm extends AuthorizingRealm {
        @Autowired
        JwtUtils jwtUtils;
        @Autowired
        UserService userService;
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof JwtToken;
        }
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            return null;
        }
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            JwtToken jwt = (JwtToken) token;
            log.info("jwt----------------->{}", jwt);
            String userId = jwtUtils.getClaimByToken((String) jwt.getPrincipal()).getSubject();
            User user = userService.getById(Long.parseLong(userId));
            if(user == null) {
                throw new UnknownAccountException("账户不存在!");
            }
            if(user.getStatus() == -1) {
                throw new LockedAccountException("账户已被锁定!");
            }
            AccountProfile profile = new AccountProfile();
            BeanUtil.copyProperties(user, profile);
            log.info("profile----------------->{}", profile.toString());
            return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName());
        }
    }
    
    

    其实主要就是doGetAuthenticationInfo登录认证这个方法,可以看到我们通过jwt获取到用户信息,判断用户的状态,最后异常就抛出对应的异常信息,否者封装成SimpleAuthenticationInfo返回给shiro。 接下来我们逐步分析里面出现的新类:

    1、shiro默认supports的是UsernamePasswordToken,而我们现在采用了jwt的方式,所以这里我们自定义一个JwtToken,来完成shiro的supports方法。

    JwtToken

    • com.gychen.shiro.JwtToken
    public class JwtToken implements AuthenticationToken {
        private String token;
        public JwtToken(String token) {
            this.token = token;
        }
        @Override
        public Object getPrincipal() {
            return token;
        }
        @Override
        public Object getCredentials() {
            return token;
        }
    }
    
    

    2、JwtUtils是个生成和校验jwt的工具类,其中有些jwt相关的密钥信息是从项目配置文件中配置的:

    /**
     * jwt工具类
     */
    @Slf4j
    @Data
    @Component
    @ConfigurationProperties(prefix = "gychen.jwt")
    public class JwtUtils {
    
        private String secret;
        private long expire;
        private String header;
    
        /**
         * 生成jwt token
         */
        public String generateToken(long userId) {
            Date nowDate = new Date();
            //过期时间
            Date expireDate = new Date(nowDate.getTime() + expire * 1000);
    
            return Jwts.builder()
                    .setHeaderParam("typ", "JWT")
                    .setSubject(userId+"")
                    .setIssuedAt(nowDate)
                    .setExpiration(expireDate)
                    .signWith(SignatureAlgorithm.HS512, secret)
                    .compact();
        }
    
        public Claims getClaimByToken(String token) {
            try {
                return Jwts.parser()
                        .setSigningKey(secret)
                        .parseClaimsJws(token)
                        .getBody();
            }catch (Exception e){
                log.debug("validate is token error ", e);
                return null;
            }
        }
    
        /**
         * token是否过期
         * @return  true:过期
         */
        public boolean isTokenExpired(Date expiration) {
            return expiration.before(new Date());
        }
    }
    
    

    3、而在AccountRealm我们还用到了AccountProfile,这是为了登录成功之后返回的一个用户信息的载体,

    AccountProfile

    • com.gychen.shiro.AccountProfile
    @Data
    public class AccountProfile implements Serializable {
        private Long id;
        private String username;
        private String avatar;
    }
    
    

    第三步:

    • ok,基本的校验的路线完成之后,我们需要少量的基本信息配置:
    server:
      port: 8081
    
    # DataSource Config
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/myvueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
        username: root
        password: 1996chen
    mybatis-plus:
      mapper-locations: classpath*:/mapper/**Mapper.xml
    
    gychen:
      jwt:
         # 加密密钥
         secret: f4e2e52034348f86b67cde581c0f9eb5
         # token有效时长,7天,单位秒
         expire: 604800
         header: token
    
    shiro-redis:
      enable: true
      redis-manager:
        host: 127.0.0.1:6379
    
    

    第四步:

    • 另外,如果你项目有使用spring-boot-devtools,需要添加一个配置文件,在resources目录下新建文件夹META-INF,然后新建文件spring-devtools.properties,这样热重启时候才不会报错。

    • resources/META-INF/spring-devtools.properties

    restart.include.shiro-redis=/shiro-[\w-\.]+jar

    第五步:

    JwtFilter

    • 定义jwt的过滤器JwtFilter。

    这个过滤器是我们的重点,这里我们继承的是Shiro内置的AuthenticatingFilter,一个可以内置了可以自动登录方法的的过滤器,有些同学继承BasicHttpAuthenticationFilter也是可以的。

    我们需要重写几个方法:

    1. createToken:实现登录,我们需要生成我们自定义支持的JwtToken
    2. onAccessDenied:拦截校验,当头部没有Authorization时候,我们直接通过,不需要自动登录;当带有的时候,首先我们校验jwt的有效性,没问题我们就直接执行executeLogin方法实现自动登录
    3. onLoginFailure:登录异常时候进入的方法,我们直接把异常信息封装然后抛出
    4. preHandle:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。

    下面我们看看总体的代码:

    • com.gychen.shiro.JwtFilter
    @Component
    public class JwtFilter extends AuthenticatingFilter {
        @Autowired
        JwtUtils jwtUtils;
        @Override
        protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            // 获取 token
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String jwt = request.getHeader("Authorization");
            if(StringUtils.isEmpty(jwt)){
                return null;
            }
            return new JwtToken(jwt);
        }
        @Override
        protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String token = request.getHeader("Authorization");
            if(StringUtils.isEmpty(token)) {
                return true;
            } else {
                // 判断是否已过期
                Claims claim = jwtUtils.getClaimByToken(token);
                if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                    throw new ExpiredCredentialsException("token已失效,请重新登录!");
                }
            }
            // 执行自动登录
            return executeLogin(servletRequest, servletResponse);
        }
        @Override
        protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            try {
                //处理登录失败的异常
                Throwable throwable = e.getCause() == null ? e : e.getCause();
                Result r = Result.fail(throwable.getMessage());
                String json = JSONUtil.toJsonStr(r);
                httpResponse.getWriter().print(json);
            } catch (IOException e1) {
            }
            return false;
        }
        /**
         * 对跨域提供支持
         */
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
            HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
            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"));
            // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
            if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
                httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
                return false;
            }
            return super.preHandle(request, response);
        }
    }
    
    

    那么到这里,我们的shiro就已经完成整合进来了,并且使用了jwt进行身份校验。

  • 相关阅读:
    Java反编译代码分析(一)
    Java信号量Semaphore
    Ubuntu SVN安装&使用&命令
    Android -- Dialog动画
    Android -- EventBus使用
    Android -- queryIntentActivities
    解决:fatal: authentication failed for https
    MySQL表名大小写敏感导致的问题
    Publish to a Linux Production Environment
    layer.js 弹窗组件API文档
  • 原文地址:https://www.cnblogs.com/nuister/p/13495360.html
Copyright © 2011-2022 走看看