关于java:shiroredisjwt整合

42次阅读

共计 10487 个字符,预计需要花费 27 分钟才能阅读完成。

一、整合流程逻辑

二、整合步骤

1. 导入 shiro-redis 的 starter 包:还有 jwt 的工具包,以及为了简化开发,我引入了 hutool 工具包。

<!--shiro-redis 整合 -->
        <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>

2. 编写配置

  • 引入 RedisSessionDAO 和 RedisCacheManager,实现将 shiro 权限数据和会话信息保留到 redis 中,实现会话共享。
  • 重写 shiro 中的 SessionManager 和 DefaultWebSecurityManager,同时在重写的 DefaultWebSecurityManager 中敞开 shiro 自 带的 session,须要设置位 false,这样用户将不能通过 session 形式登陆 shiro。前面采纳 jwt 凭证登陆。
  • 重写 shiro 的 ShiroFilterChainDefinition 注册本人的过滤器。咱们将不再通过编码方式拦挡拜访门路,而是所有门路通过本人注册的 JwtFilter 过滤器,而后判断是否有 jwt 凭证,有则登陆,无则跳过,跳过之后,有 shiro 的权限注解进行拦挡,eg:@RequiredAuthentication, 这样管制权限拜访。
@Configuration
public class ShiroConfig {
    @Autowired
    JwtFilter jwtFilter;
    /**
     * session 域治理
     * @param redisSessionDAO
     * @return
     */
    @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

        // inject redisSessionDAO
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }


    /**
     * 重写 shiro 的平安治理容器,* @param accountRealm
     * @param sessionManager
     * @param redisCacheManager
     * @return
     */
    @Bean
    public SessionsSecurityManager securityManager(AccountRealm accountRealm, SessionManager sessionManager, RedisCacheManager redisCacheManager) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);

        //inject sessionManager
        securityManager.setSessionManager(sessionManager);

        // inject redisCacheManager
        securityManager.setCacheManager(redisCacheManager);
        return securityManager;
    }

    /**
     * 定义过滤器
     * @return
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition(){
        // 申请一个默认的过滤器链
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String,String> filterMap = new LinkedHashMap<>();

        // 增加一个 jwt 过滤器到过滤器链中
        filterMap.put("/**","jwt");
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }

    /**
     * 过滤器工厂业务
     * @param securityManager shiro 中的平安治理
     * @param shiroFilterChainDefinition
     * @return
     */
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                                 ShiroFilterChainDefinition shiroFilterChainDefinition){
        /*shiro 过滤器 bean 对象 */
        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;
    }


}

3. 编写 realm

AccountRealm shiiro 进行登陆或者权限校验的逻辑。

须要重写三个办法。

  • supports:为了使 realm 反对 jwt 的凭证校验
  • doGetAuthorizationInfo: 权限校验
  • doGetAuthenticationInfo: 登陆认证校验

    @Slf4j
    @Component
    public class AccountRealm extends AuthorizingRealm{
    
      @Autowired
      JwtUtils jwtUtils;
    
      @Autowired
      UserService userService;
    
      /**
       * 判断是否为 jwt 的 token
       * @param token
       * @return
       */
      @Override
      public boolean supports(AuthenticationToken token) {return token instanceof JwtToken;}
    
      /**
       * 权限验证
       * @param principalCollection
       * @return
       */
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {return null;}
    
      /**
       * 登陆认证
       * @param authenticationToken
       * @return
       * @throws AuthenticationException
       */
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
          // 将传入的 AuthenticationToken 强转 JwtToken
          JwtToken jwtToken = (JwtToken) authenticationToken;
          // 获取 jwtToken 中的 userId
          String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();
          // 依据 jwtToken 中的 userId 查询数据库
          User user = userService.getById(Long.valueOf(userId));
          if(user == null){throw new UnknownAccountException("账户不存在!");
          }
          if(user.getStatus() == -1){throw new LockedAccountException("账户已被锁定!");
          }
          // 将能够显示的信息放在该载体中,对于明码这种隐秘信息不须要放在该载体中
          AccountProfile accountProfile = new AccountProfile();
          BeanUtils.copyProperties(user,accountProfile);
          log.info("jwt------------->{}",jwtToken);
          // 将 token 中用户的根本信息返回给 shiro
          return new SimpleAuthenticationInfo(accountProfile,jwtToken.getCredentials(),getName());
      }
    }

    次要配置 doGetAuthenticationInfo 登陆认证这个办法,通过 jwt 凭证获取用户信息,判断用户的状态,最初异样就抛出相应的异样信息。

    4. 编写 JwtToken

    shiro 默认 supports 反对的是 UsernamePasswordToken,而咱们采纳 jwt 的形式,故须要定义一个 JwtToken 来重写该 token。

    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;}
    }

    5. 编写 JwtUtils 生成和校验 jwt 的工具类

    有些 jwt 相干的密钥信息是从我的项目的配置文件中获取的。

    @Component
    @ConfigurationProperties(prefix = "mt.vuemtblog.jwt")
    public class JwtUtils {
      private String secret;
      private long expire;
      private String header;
    
      /**
       * 生成 jwt token
       * @param userId
       * @return
       */
      public static  String generateToken(long userId){return null;}
    
      /**
       * 获取 jwt 的信息
       * @param token
       * @return
       */
      public static  Claims getClaimByToken(String token){return null;}
    
      /**
       * 验证 token 是否过期
       * @param expiration
       * @return true 过期
       */
      public static boolean isTokenExpired(Date expiration){return expiration.before(new Date());
      }
    }

    6. 编写登陆胜利返回用户信息的载体 AccountProfile

    @Data
    public class AccountProfile implements Serializable {
      private Long id;
      private String username;
      private String avatar;
    }

    7. 全局配置根本信息

    shiro-redis:
    enabled: true
    redis-manger:
      host:127.0.0.1:6379
    
    mt:
    vuemtblog:
      jwt:
      #加密密钥
      secret:f4e2e52034348f86b67cde581c0f9eb5
      # token 无效时长 7 天 单位秒
      expire:604800
      # 设定 token 在 header 中的键值
      header:authorization

    8. 若我的项目应用 spring-boot-devtools,须要增加一个配置文件,

    在 resources 目录下新建 META-INF, 而后新建 spring-devtools.properties,这样热重启就不会报错。

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

    9. 编写自定义的 JwtFileter 过滤器

    这里咱们继承的是 Shiro 内置的 AuthenticatingFilter,一个能够内置了能够主动登录办法的的过滤器,有些同学继承 BasicHttpAuthenticationFilter 也是能够的。

咱们须要重写几个办法:

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

    @Component
    public class JwtFilter extends AuthenticatingFilter{
    
     @Autowired
     JwtUtils jwtUtils;
     /**
      * 实现登陆,生成自定义的 JwtToken
      * @param servletRequest
      * @param servletResponse
      * @return
      * @throws Exception
      */
     @Override
     protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {HttpServletRequest request = (HttpServletRequest)servletRequest;
         String jwt = request.getHeader("Authorization");
         if(StringUtils.isEmpty(jwt)){return null;}
         return new JwtToken(jwt);
     }
    
     /**
      *  拦挡校验
      *  @description 当头部没有 Authorization, 间接通过,不须要主动登陆。*  当带有 Authorization 时,须要先校验 jwt 的时效性,没问题间接执行 executeLogin 实现主动登陆, 将 token 委托给 shiro。* @param servletRequest
      * @param servletResponse
      * @return
      * @throws Exception
      */
     @Override
     protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {HttpServletRequest request = (HttpServletRequest) servletRequest;
         // 获取用户申请头中的 token
         String token = request.getHeader("Authorization");
         if (StringUtils.isEmpty(token)) {// 没有 token
             return true;
         } else {
             // 校验 jwt
             Claims claim = jwtUtils.getClaimByToken(token);
             // tonken 为空或者工夫过期
             if (claim == null || jwtUtils.isTokenExpired((claim.getExpiration()))) {throw new ExpiredCredentialsException("token 以生效,请从新登陆!");
             }
         }
         // 执行主动登陆
         return executeLogin(servletRequest, servletResponse);
     }
    
     /**
      * 执行登录出现异常
      * @param token
      * @param e
      * @param request
      * @param response
      * @return
      */
     @Override
     protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {HttpServletResponse httpServletResponse = (HttpServletResponse)response;
         // 1. 判断是否因异样登陆失败
         Throwable throwable = e.getCause() == null ? e : e.getCause();
         // 2. 获取登陆异样信息以自定义的 Resut 响应格局返回 json 数据
         Result result = Result.error(throwable.getMessage());
         String json = JSONUtil.toJsonStr(result);// hutool 的一个 json 工具
    
         // 3. 打印响应
         try{httpServletResponse.getWriter().print(json);
         }catch (IOException ioException){ }
         return false;
     }
    }

    三、springboot 中全局异样解决

    前后端拆散,咱们须要配置异样解决机制,返回一个敌对简略格局给前端。

解决形式:

  1. 通过 @ControllerAdvice 来进行对立异样解决
  2. 通过 @ExceptionHandler(value=RuntimeException.class) 指定要捕捉的 Exception 的各个类型,这个异样解决是全局的,所有的相似异样都会捕捉。

    /**
     * 全局异样解决
     */
    @Slf4j
    @RestControllerAdvice
    public class GlobalExcepitonHandler {
     // 捕获 shiro 的异样
     @ResponseStatus(HttpStatus.UNAUTHORIZED)
     @ExceptionHandler(ShiroException.class)
     public Result handle401(ShiroException e) {return Result.fail(401, e.getMessage(), null);
     }
     /**
      * 解决 Assert 的异样
      */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(value = IllegalArgumentException.class)
     public Result handler(IllegalArgumentException e) throws IOException {log.error("Assert 异样:-------------->{}",e.getMessage());
         return Result.fail(e.getMessage());
     }
     /**
      * @Validated 校验谬误异样解决
      */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(value = MethodArgumentNotValidException.class)
     public Result handler(MethodArgumentNotValidException e) throws IOException {log.error("运行时异样:-------------->",e);
         // 截取所有必要的错误信息;只显示谬误起因,不会显示其余 cause BY....
         BindingResult bindingResult = e.getBindingResult();
         ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
         return Result.fail(objectError.getDefaultMessage());
     }
     /*
     * 运行时异样
     */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(value = RuntimeException.class)
     public Result handler(RuntimeException e) throws IOException {log.error("运行时异样:-------------->",e);
         return Result.fail(e.getMessage());
     }
    }

    下面咱们捕获了几个异样:

    • ShiroException:shiro 抛出的异样,比方没有权限,用户登录异样
    • IllegalArgumentException:解决 Assert 的异样
    • MethodArgumentNotValidException:解决实体校验的异样
    • RuntimeException:捕获其余异样

    1. springboot 中实体校验

    应用 springboot 框架,就主动集成了 Hibernate validatior。

    第一步:实体属性上增加校验规定

    @TableName("m_user")
    public class User implements Serializable {
     private static final long serialVersionUID = 1L;
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
     
     @NotBlank(message = "昵称不能为空")
     private String username;
     private String avatar;
    
     @NotBlank(message = "邮箱不能为空")
     @Email(message = "邮箱格局不正确")
     private String email;
    }

    第二步:测试实体校验

    采纳 @Validated 注解,实体中有不合乎校验规定的,会抛出异样,在异样解决中的 MethodArgumentNotValidException 中捕捉。

    @PostMapping("/save")
     public Object save(@Validated @RequestBody User user) {return user.toString();
     }

    四、前后端拆散的跨域解决

    在后盾进行全局跨域解决

    /**
     * 解决跨域问题
     * project:vue-mt-blog
     * created by Maotao on 2020/6/30
     */
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
     @Override
     public void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").
                 allowedOrigins("*").
                 allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS").
                 allowCredentials(true).
                 maxAge(3600).
                 allowedHeaders("*");
     }
    }
    

    全局跨域解决

    /**
     * 解决跨域问题
     * project:vue-mt-blog
     * created by Maotao on 2020/6/30
     */
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
     @Override
     public void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").
                 allowedOrigins("*").
                 allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS").
                 allowCredentials(true).
                 maxAge(3600).
                 allowedHeaders("*");
     }
    }

正文完
 0