目录

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 是否合法。

导入依赖库

 1<dependency>
 2            <groupId>org.apache.shiro</groupId>
 3            <artifactId>shiro-web</artifactId>
 4            <version>1.5.3</version>
 5        </dependency>
 6        <dependency>
 7            <groupId>org.apache.shiro</groupId>
 8            <artifactId>shiro-spring</artifactId>
 9            <version>1.5.3</version>
10        </dependency>
11        <dependency>
12            <groupId>com.auth0</groupId>
13            <artifactId>java-jwt</artifactId>
14            <version>3.10.3</version>
15        </dependency>
16        <dependency>
17            <groupId>org.springframework.boot</groupId>
18            <artifactId>spring-boot-configuration-processor</artifactId>
19            <optional>true</optional>
20        </dependency>
21        <dependency>
22            <groupId>org.apache.commons</groupId>
23            <artifactId>commons-lang3</artifactId>
24            <version>3.11</version>
25        </dependency>
26        <dependency>
27            <groupId>org.apache.httpcomponents</groupId>
28            <artifactId>httpcore</artifactId>
29            <version>4.4.13</version>
30        </dependency>
31        <dependency>
32            <groupId>org.springframework.boot</groupId>
33            <artifactId>spring-boot-starter-aop</artifactId>
34        </dependency>

JwtUtil 工具类

 工具类应该包含两个方法

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

生成令牌的方法

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

验证令牌有效性的方法

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

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

1emos:
2  jwt:
3    # 密钥
4    secret: abc123456
5    # 令牌过期时间(天)
6    expire: 5
7    # 令牌缓存时间(天数)
8    cache-expire: 10

创建工具类

 1package soulboy.emos.wx.config.shiro;
 2
 3import cn.hutool.core.date.DateField;
 4import cn.hutool.core.date.DateUtil;
 5import com.auth0.jwt.JWT;
 6import com.auth0.jwt.JWTCreator;
 7import com.auth0.jwt.JWTVerifier;
 8import com.auth0.jwt.algorithms.Algorithm;
 9import com.auth0.jwt.interfaces.DecodedJWT;
10import lombok.extern.slf4j.Slf4j;
11import org.springframework.beans.factory.annotation.Value;
12import org.springframework.stereotype.Component;
13import soulboy.emos.wx.exception.EmosException;
14
15import java.util.Date;
16
17@Component
18@Slf4j
19public class JwtUtil {
20
21    @Value("${emos.jwt.secret}")
22    private String secret;
23
24    @Value("${emos.jwt.expire}")
25    private int expire;
26
27    /**
28     * 生成 token字符串
29     * token = secret + expire + userId
30     */
31    public String createToken(int userId){
32        // 过期时间:当前日期加五天之后的日期
33        Date date = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, expire);
34        // 密钥:生成密钥
35        Algorithm algorithm = Algorithm.HMAC256(secret);
36        // 创建token的内部类对象
37        JWTCreator.Builder builder = JWT.create();
38        // 生成token: secret + expire + userId (链式调用)
39        String token = builder.withClaim("userId",userId).withExpiresAt(date).sign(algorithm);
40        return token;
41    }
42
43    /**
44     * 从令牌字符串中反向解密 userId
45     */
46    public int getUserId(String token){
47        try{
48            DecodedJWT jwt = JWT.decode(token);
49            int userId = jwt.getClaim("userId").asInt();
50            return userId;
51        } catch (Exception e){
52            throw new EmosException("令牌无效");
53        }
54    }
55
56    /**
57     * 核验 token: 验证token的有效性(内容是否正确、是否过期)
58     * @param token
59     */
60    public void verifierToken(String token){
61        // 创建算法对象
62        Algorithm algorithm = Algorithm.HMAC256(secret);
63        // 创建 JWTVerifier 验证对象
64        JWTVerifier verifier = JWT.require(algorithm).build();
65        // 验证token是否有效:有异常会直接抛出Runtime异常
66        verifier.verify(token);
67    }
68}

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 接口。

 1package soulboy.emos.wx.config.shiro;
 2
 3import org.apache.shiro.authc.AuthenticationToken;
 4
 5public class OAuth2Token implements AuthenticationToken {
 6    private String token;
 7
 8    public OAuth2Token(String token) {
 9        this.token = token;
10    }
11
12    @Override
13    public Object getPrincipal() {
14        return token;
15    }
16
17    @Override
18    public Object getCredentials() {
19        return token;
20    }
21}

创建认证与授权类 AuthorizingRealm

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

 1package soulboy.emos.wx.config.shiro;
 2
 3import com.example.emos.wx.db.pojo.TbUser;
 4import com.example.emos.wx.service.UserService;
 5import org.apache.shiro.authc.*;
 6import org.apache.shiro.authz.AuthorizationInfo;
 7import org.apache.shiro.authz.SimpleAuthorizationInfo;
 8import org.apache.shiro.realm.AuthorizingRealm;
 9import org.apache.shiro.subject.PrincipalCollection;
10import org.springframework.beans.factory.annotation.Autowired;
11import org.springframework.stereotype.Component;
12
13import java.util.Set;
14
15@Component
16public class OAuth2Realm extends AuthorizingRealm {
17    // 注入JWT工具类(后续认证使用)
18    @Autowired
19    private JwtUtil jwtUtil;
20
21    /**
22     * 传入封装好的Shiro认证对象(AuthenticationToken类型)
23     * 判断类型是否符合AuthenticationToken类型
24     */
25    @Override
26    public boolean supports(AuthenticationToken token) {
27        return token instanceof OAuth2Token;
28    }
29
30    /**
31     * 授权(验证权限时调用)
32     */
33    @Override
34    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection collection) {
35        // TODO 查询用户权限列表
36        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
37        // TODO 把权限列表添加到info对象中
38        return info;
39    }
40
41    /**
42     * 认证(验证登录时调用)
43     */
44    @Override
45    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
46        // TODO 从认证类中取出token
47        // TODO 从token中取出获取userId
48        // TODO 查询该userId是否被冻结,如果被冻结则抛出自定义异常
49        // 如果验证通过:在Info对象中添加用户信息、token字符串
50        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
51        return info;
52    }
53}

设计令牌刷新机制

 我们在定义 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

 1package soulboy.emos.wx.config.shiro;
 2
 3import org.springframework.stereotype.Component;
 4
 5@Component
 6public class ThreadLocalToken {
 7    // 只需定义ThreadLocal变量即可
 8    private ThreadLocal<String> local = new ThreadLocal<>();
 9
10    public void setToken(String token){
11        local.set(token);
12    }
13
14    public String getToken(){
15        return (String) local.get();
16    }
17
18    public void clear(){
19        local.remove();
20    }
21}

创建 AuthenticatingFilter 类

OAuth2Filter

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

1emos:
2  jwt:
3    # 密钥
4    secret: abc123456
5    # 令牌过期时间(天)
6    expire: 5
7    # 令牌缓存时间(天数)
8    cache-expire: 10

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

  1package soulboy.emos.wx.config.shiro;
  2
  3import cn.hutool.core.util.StrUtil;
  4import com.auth0.jwt.exceptions.JWTDecodeException;
  5import com.auth0.jwt.exceptions.TokenExpiredException;
  6import org.apache.http.HttpStatus;
  7import org.apache.shiro.authc.AuthenticationException;
  8import org.apache.shiro.authc.AuthenticationToken;
  9import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
 10import org.springframework.beans.factory.annotation.Autowired;
 11import org.springframework.beans.factory.annotation.Value;
 12import org.springframework.context.annotation.Scope;
 13import org.springframework.data.redis.core.RedisTemplate;
 14import org.springframework.stereotype.Component;
 15import org.springframework.web.bind.annotation.RequestMethod;
 16
 17import javax.servlet.FilterChain;
 18import javax.servlet.ServletException;
 19import javax.servlet.ServletRequest;
 20import javax.servlet.ServletResponse;
 21import javax.servlet.http.HttpServletRequest;
 22import javax.servlet.http.HttpServletResponse;
 23import java.io.IOException;
 24import java.util.concurrent.TimeUnit;
 25
 26@Component
 27@Scope("prototype") //多例对象
 28public class OAuth2Filter extends AuthenticatingFilter {
 29    @Autowired
 30    private ThreadLocalToken threadLocalToken;
 31
 32    @Value("${emos.jwt.cache-expire}")
 33    private int cacheExpire;
 34
 35    @Autowired
 36    private JwtUtil jwtUtil;
 37
 38    // 操作redis
 39    @Autowired
 40    private RedisTemplate redisTemplate;
 41
 42    /**
 43     * 将token字符串封装成令牌对象(AuthenticationToken)
 44     * 拦截那些应该被Shiro处理的请求
 45     * 并从请求里面获取token,封装为AuthenticationToken返回给Shiro框架
 46     */
 47    @Override
 48    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
 49        HttpServletRequest req= (HttpServletRequest) request;
 50        // 获取token字符串
 51        String token=getRequestToken(req);
 52        // 如果令牌字符串是空,则直接返回null,null是无法进行封装的
 53        if(StrUtil.isBlank(token)){
 54            return null;
 55        }
 56        // 如果不是空,生成令牌对象,交给Shiro框架的AuthorizingRealm使用
 57        return new OAuth2Token(token);
 58    }
 59
 60    /**
 61     * 拦截请求,判断请求是否需要被Shiro处理
 62     */
 63    @Override
 64    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
 65        HttpServletRequest req = (HttpServletRequest) request;
 66        // 放行Option类型的请求: Ajax提交application/json数据的时候,会先发出Option请求
 67        if(req.getMethod().equals(RequestMethod.OPTIONS.name())){
 68            return true;
 69        }
 70        // 其他请求类型一律不放行(需要被Shrio进行处理)
 71        return false;
 72    }
 73
 74    /**
 75     * Shiro处理请求的方法
 76     */
 77    @Override
 78    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
 79        HttpServletRequest req = (HttpServletRequest) request;
 80        HttpServletResponse resp = (HttpServletResponse) response;
 81        resp.setContentType("text/html");
 82        resp.setCharacterEncoding("UTF-8");
 83        // 设置支持跨域的参数(向后端分离项目,后端项目需要在响应里面需要加入此类参数)
 84        resp.setHeader("Access-Control-Allow-Credentials", "true");
 85        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
 86
 87        // 先清除当前线程的threadLocal属性(媒介类)
 88        threadLocalToken.clear();
 89
 90        // 获取到token字符串
 91        String token = getRequestToken(req);
 92        // 如果是空字符串,则返回HTTP对应的状态消息(SC_UNAUTHORIZED = 401)
 93        if(StrUtil.isBlank(token)){
 94            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
 95            resp.getWriter().print("无效的令牌");
 96            return false;
 97        }
 98        try{
 99            // 如果token内容不为空(验证内容是否有效,是否过期)
100            jwtUtil.verifierToken(token);
101        }catch (TokenExpiredException e){  //客户端令牌过期
102            // Redis的token没有过期 (续约)
103            if(redisTemplate.hasKey(token)){
104                // 把老token删除
105                redisTemplate.delete(token);
106                int userId=jwtUtil.getUserId(token);
107                // 生成新的token
108                token = jwtUtil.createToken(userId);
109                // 保存到redis中
110                redisTemplate.opsForValue().set(token,userId+"",cacheExpire, TimeUnit.DAYS);
111                // 写入threadLocalToken
112                threadLocalToken.setToken(token);
113            }
114            // Redis的token过期了(客户端服务器端都过期了),需要用户重新登录
115            else {
116                resp.setStatus(HttpStatus.SC_UNAUTHORIZED); //SC_UNAUTHORIZED = 401;
117                resp.getWriter().print("令牌已过期");
118                return false;
119            }
120        }catch (JWTDecodeException e){ //客户端提交的令牌字符串内容不对(伪造的)
121            // 如果令牌过期
122            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
123            resp.getWriter().print("无效的令牌");
124            return false;
125        }
126
127        // 通过检测,令牌没有问题,则需要将令牌传入Realm对象中继续执行(认证和授权)
128        // 认证和授权的结果是成功还是失败 bool   false代表失败 true代表成功
129        // bool == false 的时候会执行 onLoginFailure() 方法
130        boolean bool = executeLogin(request,response);
131        return bool;
132    }
133
134    /**
135     * Shiro认证失败会触发刚方法
136     */
137    @Override
138    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
139        HttpServletRequest req = (HttpServletRequest) request;
140        HttpServletResponse resp = (HttpServletResponse) response;
141        resp.setContentType("text/html");
142        resp.setCharacterEncoding("UTF-8");
143        // 支持跨域
144        resp.setHeader("Access-Control-Allow-Credentials", "true");
145        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
146        // 返回错误消息状态   int SC_UNAUTHORIZED = 401;
147        resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
148
149        try{
150            // 认证失败的详细消息返回
151            resp.getWriter().print(e.getMessage());
152        }catch (Exception exception){
153            exception.printStackTrace();
154        }
155        //认证失败 一定是false
156        return false;
157    }
158
159    @Override
160    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
161        HttpServletRequest req = (HttpServletRequest) request;
162        HttpServletResponse resp = (HttpServletResponse) response;
163        resp.setContentType("text/html");
164        resp.setCharacterEncoding("UTF-8");
165        resp.setHeader("Access-Control-Allow-Credentials", "true");
166        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
167        // 父类doFilterInternal()掌管拦截请求和返回相应的方法,和传统的Filter类一样
168        super.doFilterInternal(request, response, chain);
169    }
170
171    /**
172     * 获取token字符串
173     */
174    private String getRequestToken(HttpServletRequest request){
175        String token=request.getHeader("token");
176        if(StrUtil.isBlank(token)){
177            token=request.getParameter("token");
178        }
179        return token;
180    }
181}

创建 ShiroConfig

 ShiroConfig

soulboy.emos.wx.config.shiro.ShiroConfig

 1package soulboy.emos.wx.config.shiro;
 2
 3import org.apache.shiro.mgt.SecurityManager;
 4import org.apache.shiro.spring.LifecycleBeanPostProcessor;
 5import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
 6import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
 7import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
 8import org.springframework.context.annotation.Bean;
 9import org.springframework.context.annotation.Configuration;
10
11import javax.servlet.Filter;
12import java.util.HashMap;
13import java.util.LinkedHashMap;
14import java.util.Map;
15
16@Configuration
17public class ShiroConfig {
18    /**
19     * 用于封装Realm对象
20     */
21    @Bean("securityManager")
22    public SecurityManager securityManager(OAuth2Realm realm){
23        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
24        securityManager.setRealm(realm);
25        securityManager.setRememberMeManager(null);
26        return securityManager;
27    }
28
29    /**
30     * 用于封装Filter对象
31     */
32    @Bean("shiroFilter")
33    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,OAuth2Filter filter){
34        ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
35        shiroFilter.setSecurityManager(securityManager);
36        Map<String , Filter> map = new HashMap<>();
37        map.put("oauth2",filter);
38        // 名字叫作oauth2
39        shiroFilter.setFilters(map);
40        // 定义不需要拦截的请求路径(anon代表不需要拦截)
41        Map<String,String> filterMap = new LinkedHashMap<>();
42        filterMap.put("/webjars/**", "anon");
43        filterMap.put("/druid/**", "anon");
44        filterMap.put("/app/**", "anon");
45        filterMap.put("/sys/login", "anon");
46        filterMap.put("/swagger/**", "anon");
47        filterMap.put("/v2/api-docs", "anon");
48        filterMap.put("/swagger-ui.html", "anon");
49        filterMap.put("/swagger-resources/**", "anon");
50        filterMap.put("/captcha.jpg", "anon");
51        filterMap.put("/user/register", "anon");
52        filterMap.put("/user/login", "anon");
53        filterMap.put("/test/**", "anon");
54        filterMap.put("/meeting/recieveNotify", "anon");
55        // 定义需要拦截的请求路径(都需要被 oauth2过滤器拦截)
56        filterMap.put("/**", "oauth2");
57        // 将普通的JavaBean(OAuth2Filter)注册为Shiro框架的拦截器
58        shiroFilter.setFilterChainDefinitionMap(filterMap);
59        return shiroFilter;
60    }
61
62    /**
63     * 管理Shiro对象生命周期
64     */
65    @Bean("lifecycleBeanPostProcessor")
66    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
67        return new LifecycleBeanPostProcessor();
68    }
69
70    /**
71     * AOP切面类,Controller方法执行前,权限校验
72     * 需要传入SecurityManager(Realm对象)
73     */
74    @Bean
75    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
76        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
77        advisor.setSecurityManager(securityManager);
78        return advisor;
79    }
80}

创建 AOP 切面类

soulboy/emos/wx/aop/TokenAspect.java

 1package soulboy.emos.wx.aop;
 2
 3
 4import org.aspectj.lang.ProceedingJoinPoint;
 5import org.aspectj.lang.annotation.Around;
 6import org.aspectj.lang.annotation.Aspect;
 7import org.aspectj.lang.annotation.Pointcut;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.stereotype.Component;
10import soulboy.emos.wx.common.util.R;
11import soulboy.emos.wx.config.shiro.ThreadLocalToken;
12
13@Aspect
14@Component
15public class TokenAspect {
16    @Autowired
17    private ThreadLocalToken threadLocalToken;
18
19    @Pointcut("execution(public * com.example.emos.wx.controller.*.*(..))")
20    public void aspect(){
21
22    }
23
24    @Around("aspect()")
25    public Object around(ProceedingJoinPoint point) throws Throwable{
26        R r=(R)point.proceed(); //Controller目标方法的返回值,返回值一定是R类型
27        String token = threadLocalToken.getToken();
28        // threadLocalToken 有没有绑定新的token,如果有一定是最新的(续约写入ThreadLocal的)
29        if(token!=null){
30            // 将最新的token封装到响应结果R(返回给客户端)
31            r.put("token",token);
32            threadLocalToken.clear();
33        }
34        return r;
35    }
36}

精简返回客户端的异常

精简异常

soulboy.emos.wx.config.ExceptionAdvice

 1package soulboy.emos.wx.config;
 2
 3import lombok.extern.slf4j.Slf4j;
 4import org.apache.shiro.authz.UnauthorizedException;
 5import org.springframework.http.HttpStatus;
 6import org.springframework.web.bind.MethodArgumentNotValidException;
 7import org.springframework.web.bind.annotation.ExceptionHandler;
 8import org.springframework.web.bind.annotation.ResponseBody;
 9import org.springframework.web.bind.annotation.ResponseStatus;
10import org.springframework.web.bind.annotation.RestControllerAdvice;
11import soulboy.emos.wx.exception.EmosException;
12
13@Slf4j
14@RestControllerAdvice
15public class ExceptionAdvice {
16    @ResponseBody
17    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
18    @ExceptionHandler(Exception.class) // 捕获SpringMVC全局的异常(只要是Exception子类的异常都可以捕获)
19    public String exceptionHandler(Exception e){
20        log.error("执行异常",e);
21        if(e instanceof MethodArgumentNotValidException){ // 后端验证失败异常
22            MethodArgumentNotValidException exception= (MethodArgumentNotValidException) e;
23            return exception.getBindingResult().getFieldError().getDefaultMessage(); //获取异常消息
24        }
25        else if(e instanceof EmosException){ // EmosException 精简异常内容
26            EmosException exception= (EmosException) e;
27            return exception.getMsg();
28        }
29        else if(e instanceof UnauthorizedException){ // Emos异常
30            return "你不具备相关权限";
31        }
32        else{ //普通后端异常
33            return "后端执行异常";
34        }
35    }
36}

全局异常测试


作者:Soulboy