目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

单点登录(Single Sign On):Shiro➕JWT

认证

 认证就是要核验用户的身份,比如说通过用户名和密码来检验用户的身份。说简单一些,认证就是登陆。登陆之后Shiro要记录用户成功登陆的凭证。

授权

 没有授权机制的web系统是很危险的,所有用户在登录成功之后都是超级管理员,都可以调用CRUD的API。
 授权是比认证更加精细度的划分用户的行为。比如说一个教务管理系统中,学生登陆之后只能查看信息,不能修改信息。而班主任就可以修改学生的信息。这就是利用授权来限定不同身份用户的行为。

Shiro靠什么做认证与授权的?

 Shiro可以利用 HttpSession 或者 Redis 存储用户的登陆凭证,以及角色或者身份信息。
 利用过滤器 (Filter),对每个Http请求过滤,检查请求对应的 HttpSession 或者 Redis 中的认证与授权信息。如果用户没有登陆,或者权限不够,那么Shiro会向客户端返回错误信息。
 也就是说,我们写用户登陆模块的时候,用户登陆成功之后,要调用Shiro保存登陆凭证。然后查询用户的角色和权限,让Shiro存储起来。将来不管哪个方法需要登陆访问,或者拥有特定的角色跟权限才能访问,我们在方法前设置注解即可,非常简单。

JWT

 JWT (Json Web Token),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
 JWT一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT兼容更多的客户端
 传统的 HttpSession 依靠浏览器的 Cookie 存放 SessionId ,所以要求客户端必须是浏览器。现在的JavaWeb系统,客户端可以是浏览器、APP、小程序,以及物联网设备。为了让这些设备都能访问到JavaWeb项目,就必须要引入JWT技术。JWT的 Tken 是纯字符串,至于客户端怎么保存,没有具体要求。只要客户端发起请求的时候,附带上 Token 即可。所以像物联网设备,我们可以用 soLite 存储 Token 数据。``

Shiro与JWT如何搭配使用

  1. JWT先生成token(令牌)。
  2. Shiro的认证服务检查token是否合法。

导入依赖库

<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.5.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.5.3</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.4.13</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

JwtUtil工具类

 工具类应该包含两个方法

1.生成token的方法
2.检查token的方法

生成令牌的方法

生成令牌的素材描述
密钥在application.yml配置文件中进行设置
过期时间在application.yml配置文件中进行设置
用户ID

验证令牌有效性的方法

需要验证的点描述
token的内容是否有效
是否过期

定义密钥和过期时间
 application.yml

emos:
  jwt:
    # 密钥
    secret: abc123456
    # 令牌过期时间(天)
    expire: 5
    # 令牌缓存时间(天数)
    cache-expire: 10

创建工具类

package soulboy.emos.wx.config.shiro;

import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import soulboy.emos.wx.exception.EmosException;

import java.util.Date;

@Component
@Slf4j
public class JwtUtil {

    @Value("${emos.jwt.secret}")
    private String secret;

    @Value("${emos.jwt.expire}")
    private int expire;

    /**
     * 生成 token字符串
     * token = secret + expire + userId
     */
    public String createToken(int userId){
        // 过期时间:当前日期加五天之后的日期
        Date date = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, expire);
        // 密钥:生成密钥
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 创建token的内部类对象
        JWTCreator.Builder builder = JWT.create();
        // 生成token: secret + expire + userId (链式调用)
        String token = builder.withClaim("userId",userId).withExpiresAt(date).sign(algorithm);
        return token;
    }

    /**
     * 从令牌字符串中反向解密 userId
     */
    public int getUserId(String token){
        try{
            DecodedJWT jwt = JWT.decode(token);
            int userId = jwt.getClaim("userId").asInt();
            return userId;
        } catch (Exception e){
            throw new EmosException("令牌无效");
        }
    }

    /**
     * 核验 token: 验证token的有效性(内容是否正确、是否过期)
     * @param token
     */
    public void verifierToken(String token){
        // 创建算法对象
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 创建 JWTVerifier 验证对象
        JWTVerifier verifier = JWT.require(algorithm).build();
        // 验证token是否有效:有异常会直接抛出Runtime异常
        verifier.verify(token);
    }
}

Shiro相关类的功能作用

需要用到的类(类名无所谓)描述信息
ShiroConfig把设置应用到Shiro框架,让springboot框架可以识别Shiro
AuthenticatingFilter拦截HTTP请求,验证Token
AuthorizingRealm定义认证与授权的实现方法
AuthenticationToken把Token封装认证对象

1.AuthenticatingFilter 拦截HTTP请求,拦截token字符串
2.把token封装成认证对象(AuthenticationToken)
3.把认证对象传入 AuthorizingRealm 进行认证和授权

将token封装成Shiro可以识别的认证对象

 Shiro框架无法直接接收token(令牌字符串)
 JwtUtil生成token字符串,token字符串返回给客户端,后续客户端在发起请求的时候需要带上token字符串,后端项目获取令牌之后不能直接交给Shiro框架做认证,后端项目必须把token字符串封装成Shiro项目可以识别的认证对象(AuthenticationToken)
 

OAuth2Token

 认证类需要实现 AuthenticationToken 接口。

package soulboy.emos.wx.config.shiro;

import org.apache.shiro.authc.AuthenticationToken;

public class OAuth2Token implements AuthenticationToken {
    private String token;

    public OAuth2Token(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

创建认证与授权类AuthorizingRealm

1.创建AuthorizingRealm类的子类
2.实现认证与授权的方法

package soulboy.emos.wx.config.shiro;

import com.example.emos.wx.db.pojo.TbUser;
import com.example.emos.wx.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
public class OAuth2Realm extends AuthorizingRealm {
    // 注入JWT工具类(后续认证使用)
    @Autowired
    private JwtUtil jwtUtil;

    /**
     * 传入封装好的Shiro认证对象(AuthenticationToken类型)
     * 判断类型是否符合AuthenticationToken类型
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof OAuth2Token;
    }

    /**
     * 授权(验证权限时调用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection collection) {
        // TODO 查询用户权限列表
        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
        // TODO 把权限列表添加到info对象中
        return info;
    }

    /**
     * 认证(验证登录时调用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // TODO 从认证类中取出token
        // TODO 从token中取出获取userId
        // TODO 查询该userId是否被冻结,如果被冻结则抛出自定义异常
        // 如果验证通过:在Info对象中添加用户信息、token字符串
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
        return info;
    }
}

设计令牌刷新机制

 我们在定义JwtUtil工具类的时候,生成的 Token 都有过期时间。那么问题来了,假设 Token 过期时间为15天,用户在第14天的时候,还可以免登录正常访问系统。但是到了第15天,用户的Token过期,于是用户需要重新登录系统
 Httpsession 的过期时间比较优雅,默认为15分钟。如果用户连续使用系统,只要间隔时间不超过15分钟,系统就不会销毁 Httpsession 对象。JWT的令牌过期时间能不能做成Httpsession 那样超时时间,只要用户间隔操作时间不超过15天,系统就不需要用户重新登录系统。

为什么要刷新令牌?
 令牌保存在客户端。
 用户一直在登陆使用系统,不会重新生成令牌。
 令牌到期,用户必须重新登陆。
 令牌应该自动续期,常见的解决方案有两种:双令牌机制。

双令牌机制
 设置长短时间的令牌
 A. 短日期令牌失效,就用长日期令牌,认证通过并生成新的令牌返回给客户端。
 B. 短日期令牌失效,长日期令牌失效,则用户必须重新登陆。

缓存令牌机制
 令牌缓存到Redis上面(Redis缓存令牌过期时间是客户端令牌过期时间的一倍),客户端5天,Redis缓存10天。
 A. 如果客户端令牌过期,Redis缓存令牌没有过期,认证通过并生成新的令牌返回给客户端并同步至Redis。(5~10天发出的请求)
 B. 如果客户端令牌过期,Redis缓存令牌也过期了,则用户必须重新登陆。(10天以后发出的请求)

双令牌机制 VS 缓存令牌机制
 缓存令牌机制在架构上引入了Redis,因而在架构上会复杂一些。
 在双令牌机制下,客户端请求会附带两个令牌,后端系统需要判断,应该使用短日期的令牌?还是使用长日期的令牌?必须要系统加以判断才能使用。

客户端如何更新令牌

 在我们的方案中,服务端刷新 Token 过期时间,其实就是生成一个新的 Token 给客户端。那么客户端怎么知道这次响应带回来的Token是更新过的呢? 这个问题很容易解决。
 saveToken

 只要用户成功登陆系统,当后端服务器更新 Token 的时候,就在响应中添加 Token 。客户端那边判断每次Aiax响应里面是否包含 Token如果包含,就把 Token 保存起来就可以了.

如何把续期的新token写入到response响应中

responseToken
 定义 OAuth2Filter类拦截所有的HTTP请求,一方面它会把请求中的 Token 字符串提取出来,封装成对象交给Shiro框架;另一方面,它会检查 Token 的有效性。如果 Token 过期,那么会生成新的 Token ,分别存储在 ThreadLocalToken 和 Redis 中。
之所以要把 新合牌 保存到 ThreadLocalToken 里面,是因为要向 AOP切面类 传递这个 新爷牌 。虽然 OAuth2Filter 中有 dofilterInternal() 方法,我们可以得到响应并且写入 新爷牌 。但是这个做非常麻烦,首先我们要通过IO流读取响应中的数据,然后还要把数据解析成JSON对象,最后再放入这个新令牌。如果我们定义了 AOP切面类 ,拦截所有Web方法返回的 R对象 ,然后在 R对象 里面添加 新牌 ,这多简单啊。但是 OAuth2Filter 和 AOP 切面类之间没有调用关系,所以我们很难把 新牌 传给 AOP切面类
 Filter可以拦截请求,也可以拦截响应。
 客户端令牌过期,服务端令牌没过期,生成新的token,并写入ThreadLocalToken对象中(ThreadLocal可以为每一个线程分配一个独立空间)。
 

创建存储令牌的媒介类ThreadLocalToken

 soulboy.emos.wx.config.shiro.ThreadLocalToken

package soulboy.emos.wx.config.shiro;

import org.springframework.stereotype.Component;

@Component
public class ThreadLocalToken {
    // 只需定义ThreadLocal变量即可
    private ThreadLocal<String> local = new ThreadLocal<>();

    public void setToken(String token){
        local.set(token);
    }

    public String getToken(){
        return (String) local.get();
    }

    public void clear(){
        local.remove();
    }
}

创建AuthenticatingFilter类

OAuth2Filter

 在配置文件中添加JWT需要用到的密钥、过期时间、缓存过期时间。

emos:
  jwt:
    # 密钥
    secret: abc123456
    # 令牌过期时间(天)
    expire: 5
    # 令牌缓存时间(天数)
    cache-expire: 10

soulboy/emos/wx/config/shiro/OAuth2Filter.java

package soulboy.emos.wx.config.shiro;

import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

@Component
@Scope("prototype") //多例对象
public class OAuth2Filter extends AuthenticatingFilter {
    @Autowired
    private ThreadLocalToken threadLocalToken;

    @Value("${emos.jwt.cache-expire}")
    private int cacheExpire;

    @Autowired
    private JwtUtil jwtUtil;

    // 操作redis
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 将token字符串封装成令牌对象(AuthenticationToken)
     * 拦截那些应该被Shiro处理的请求
     * 并从请求里面获取token,封装为AuthenticationToken返回给Shiro框架
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req= (HttpServletRequest) request;
        // 获取token字符串
        String token=getRequestToken(req);
        // 如果令牌字符串是空,则直接返回null,null是无法进行封装的
        if(StrUtil.isBlank(token)){
            return null;
        }
        // 如果不是空,生成令牌对象,交给Shiro框架的AuthorizingRealm使用
        return new OAuth2Token(token);
    }

    /**
     * 拦截请求,判断请求是否需要被Shiro处理
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest req = (HttpServletRequest) request;
        // 放行Option类型的请求: Ajax提交application/json数据的时候,会先发出Option请求
        if(req.getMethod().equals(RequestMethod.OPTIONS.name())){
            return true;
        }
        // 其他请求类型一律不放行(需要被Shrio进行处理)
        return false;
    }

    /**
     * Shiro处理请求的方法
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        // 设置支持跨域的参数(向后端分离项目,后端项目需要在响应里面需要加入此类参数)
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));

        // 先清除当前线程的threadLocal属性(媒介类)
        threadLocalToken.clear();

        // 获取到token字符串
        String token = getRequestToken(req);
        // 如果是空字符串,则返回HTTP对应的状态消息(SC_UNAUTHORIZED = 401)
        if(StrUtil.isBlank(token)){
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }
        try{
            // 如果token内容不为空(验证内容是否有效,是否过期)
            jwtUtil.verifierToken(token);
        }catch (TokenExpiredException e){  //客户端令牌过期
            // Redis的token没有过期 (续约)
            if(redisTemplate.hasKey(token)){
                // 把老token删除
                redisTemplate.delete(token);
                int userId=jwtUtil.getUserId(token);
                // 生成新的token
                token = jwtUtil.createToken(userId);
                // 保存到redis中
                redisTemplate.opsForValue().set(token,userId+"",cacheExpire, TimeUnit.DAYS);
                // 写入threadLocalToken
                threadLocalToken.setToken(token);
            }
            // Redis的token过期了(客户端服务器端都过期了),需要用户重新登录
            else {
                resp.setStatus(HttpStatus.SC_UNAUTHORIZED); //SC_UNAUTHORIZED = 401;
                resp.getWriter().print("令牌已过期");
                return false;
            }
        }catch (JWTDecodeException e){ //客户端提交的令牌字符串内容不对(伪造的)
            // 如果令牌过期
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }

        // 通过检测,令牌没有问题,则需要将令牌传入Realm对象中继续执行(认证和授权)
        // 认证和授权的结果是成功还是失败 bool   false代表失败 true代表成功
        // bool == false 的时候会执行 onLoginFailure() 方法
        boolean bool = executeLogin(request,response);
        return bool;
    }

    /**
     * Shiro认证失败会触发刚方法
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        // 支持跨域
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        // 返回错误消息状态   int SC_UNAUTHORIZED = 401;
        resp.setStatus(HttpStatus.SC_UNAUTHORIZED);

        try{
            // 认证失败的详细消息返回
            resp.getWriter().print(e.getMessage());
        }catch (Exception exception){
            exception.printStackTrace();
        }
        //认证失败 一定是false
        return false;
    }

    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        // 父类doFilterInternal()掌管拦截请求和返回相应的方法,和传统的Filter类一样
        super.doFilterInternal(request, response, chain);
    }

    /**
     * 获取token字符串
     */
    private String getRequestToken(HttpServletRequest request){
        String token=request.getHeader("token");
        if(StrUtil.isBlank(token)){
            token=request.getParameter("token");
        }
        return token;
    }
}

创建ShiroConfig

 ShiroConfig

soulboy.emos.wx.config.shiro.ShiroConfig

package soulboy.emos.wx.config.shiro;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    /**
     * 用于封装Realm对象
     */
    @Bean("securityManager")
    public SecurityManager securityManager(OAuth2Realm realm){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setRememberMeManager(null);
        return securityManager;
    }

    /**
     * 用于封装Filter对象
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,OAuth2Filter filter){
        ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        Map<String , Filter> map = new HashMap<>();
        map.put("oauth2",filter);
        // 名字叫作oauth2
        shiroFilter.setFilters(map);
        // 定义不需要拦截的请求路径(anon代表不需要拦截)
        Map<String,String> filterMap = new LinkedHashMap<>();
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/app/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/user/register", "anon");
        filterMap.put("/user/login", "anon");
        filterMap.put("/test/**", "anon");
        filterMap.put("/meeting/recieveNotify", "anon");
        // 定义需要拦截的请求路径(都需要被 oauth2过滤器拦截)
        filterMap.put("/**", "oauth2");
        // 将普通的JavaBean(OAuth2Filter)注册为Shiro框架的拦截器
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }

    /**
     * 管理Shiro对象生命周期
     */
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    /**
     * AOP切面类,Controller方法执行前,权限校验
     * 需要传入SecurityManager(Realm对象)
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

创建AOP切面类

soulboy/emos/wx/aop/TokenAspect.java

package soulboy.emos.wx.aop;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import soulboy.emos.wx.common.util.R;
import soulboy.emos.wx.config.shiro.ThreadLocalToken;

@Aspect
@Component
public class TokenAspect {
    @Autowired
    private ThreadLocalToken threadLocalToken;

    @Pointcut("execution(public * com.example.emos.wx.controller.*.*(..))")
    public void aspect(){

    }

    @Around("aspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable{
        R r=(R)point.proceed(); //Controller目标方法的返回值,返回值一定是R类型
        String token = threadLocalToken.getToken();
        // threadLocalToken 有没有绑定新的token,如果有一定是最新的(续约写入ThreadLocal的)
        if(token!=null){
            // 将最新的token封装到响应结果R(返回给客户端)
            r.put("token",token);
            threadLocalToken.clear();
        }
        return r;
    }
}

精简返回客户端的异常

精简异常

soulboy.emos.wx.config.ExceptionAdvice

package soulboy.emos.wx.config;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import soulboy.emos.wx.exception.EmosException;

@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class) // 捕获SpringMVC全局的异常(只要是Exception子类的异常都可以捕获)
    public String exceptionHandler(Exception e){
        log.error("执行异常",e);
        if(e instanceof MethodArgumentNotValidException){ // 后端验证失败异常
            MethodArgumentNotValidException exception= (MethodArgumentNotValidException) e;
            return exception.getBindingResult().getFieldError().getDefaultMessage(); //获取异常消息
        }
        else if(e instanceof EmosException){ // EmosException 精简异常内容
            EmosException exception= (EmosException) e;
            return exception.getMsg();
        }
        else if(e instanceof UnauthorizedException){ // Emos异常
            return "你不具备相关权限";
        }
        else{ //普通后端异常
            return "后端执行异常";
        }
    }
}

全局异常测试


作者:Soulboy