一、整合流程逻辑

二、整合步骤

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,这样管制权限拜访。
@Configurationpublic 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@Componentpublic 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

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

    7. 全局配置根本信息

    shiro-redis:enabled: trueredis-manger:  host:127.0.0.1:6379mt: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之前就被限度了。

    @Componentpublic 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@RestControllerAdvicepublic 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 */@Configurationpublic 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 */@Configurationpublic 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("*"); }}