shiro-redis-jwt整合

一、整合流程逻辑

20201031132733189.png

二、整合步骤

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:登陆认证校验
```java
@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();
      }

      四、前后端分离的跨域处理

      在后台进行全局跨域处理 ```java /**

    • 解决跨域问题

    • 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("*");

      } }

全局跨域处理
```java
/**
 * 解决跨域问题
 * 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("*");
    }
}

本文由博客一文多发平台 OpenWrite 发布!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值