Shiro和JWT技术

一、Shiro简介

Shiro是Java领域非常知名的认证(Authentication)与授权(Authorization
)框架,用以替代JavaEE中的JAAS功能。相较于其他认证与授权框架,Shiro设计的非常简单,所以广受好评。任意JavaWeb项目都可以使用Shiro框架,而Spring Security必须要使用在Spring项目中。所以Shiro的适用性更加广泛。JFinal和Nutz非Spring框架都可以使用Shiro,而不能使用Spring Security框架。

什么是认证?

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

什么是授权?

​ 授权是比认证更加精细度的划分用户的行为。比如说一个教务管理系统中,学生登陆之后只能查看信息,不能修改信息。而班主任就可以修改学生的信息。这就是利用授权来限定不同身份用户的行为。

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

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

二、JWT简介

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

JWT可以用在单点登录的系统中

传统的JavaWeb项目,利用HttpSession保存用户的登陆凭证。如果后端系统采用了负载均衡设计,当用户在A节点成功登陆,那么登陆凭证保存在A节点的HttpSession中。如果用户下一个请求被负载均衡到了B节点,因为B节点上面没有用户的登陆凭证,所以需要用户重新登录,这个体验太糟糕了。

​ 如果用户的登陆凭证经过加密(Token)保存在客户端,客户端每次提交请求的时候,把Token上传给后端服务器节点。即便后端项目使用了负载均衡,每个后端节点接收到客户端上传的Token之后,经过检测,是有效的Token,于是就断定用户已经成功登陆,接下来就可以提供后端服务了。

JWT兼容更多的客户端

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

三、创建JWT工具类

JWT的Token要经过加密才能返回给客户端,包括客户端上传的Token,后端项目需要验证核实。于是我们需要一个JWT工具类,用来加密Token和验证Token的有效性。

3.1导入依赖库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<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>

3.2、定义密钥和过期时间

放在yml文件里,进行值注入

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

3.3、工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

@Component
@Slf4j
public class JwtUtil {
@Value("${aaa.jwt.secret}")
private String secret;
@Value("${aaa.jwt.expire}")
private int expire;

public String createToken(int userId) {
// DateField.DAY_OF_YEAR单位为天,5天后过期
Date date = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, 5);
// HMAC256加密,
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTCreator.Builder builder = JWT.create();
String token = builder.withClaim("userId", userId).withExpiresAt(date).sign(algorithm);

return token;

}


public int getUserId(String token) {
DecodedJWT jwt = JWT.decode(token);
int userId = jwt.getClaim("userId").asInt();
return userId;
}

// 校验
public void verify(String token) {
Algorithm algorithm=Algorithm.HMAC256(secret);
JWTVerifier verifier=JWT.require(algorithm).build();
verifier.verify(token);


}



}

四、令牌封装为认证对象

我们要把JWT和Shiro框架对接起来,这样Shiro框架就会拦截所有的Http请求,然后验证请求提交的
Token是否有效。

客户端提交的Token不能直接交给Shiro框架,需要先封装成AuthenticationToken 类型的对象,所以我们我们需要先创建AuthenticationToken的实现类。

1
过滤器拦截HTTP请求,把拦截下来的token字符串封装到token对象中,把token对象传给Realm类去做认证。

封装token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.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;
}
}

创建OAuth2Realm类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63


import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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;

@Component
public class OAuth2Realm extends AuthorizingRealm{


@Autowired
private JwtUtil jwtUtil;


/**
* 判断是否支持此token(令牌的类型)
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;
}

/**
* 授权(验证权限时调用)
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
// TODO 查询用户权限列表
// TODO 往info里添加权限列表

return info;
}

/**
* 认证(登录时调用)
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

//TODO 从令牌获取userid,检验用户是否被冻结
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();

// TODO 往info里添加信息,token字符串,返回给shiro

return info;
}
}

刷新令牌如何设计

一、为什么要刷新Token的过期时间?

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

  • 双Token

  • Token缓存

  • 双令牌机制

    • 设置长短日期的令牌
    • 短日期令牌失效就用长日期
  • 缓存令牌机制

    • 令牌缓存到redis上
    • 缓存的过期时间是客户端令牌的一倍
    • 如果客户端令牌过期,缓存的令牌没有过期,则生成新的令牌
    • 全都过期就重新登陆

Token缓存方案是把Token缓存到Redis,然后设置Redis里面缓存的Token过期时间为正常Token的1倍,然后根据情况刷新Token的过期时间。

Token失效,缓存也不存在的情况:
当第15天,用户的Token失效以后,我们让Shiro程序到Redis查看是否存在缓存的Token,如果这个Token不存在于Redis里面,就说明用户的操作间隔了15天,需要重新登录。

Token失效,但是缓存还存在的情况:
如果Redis中存在缓存的Token,说明当前Token失效后,间隔时间还没有超过15天,不应该让用户重新登录。所以要生成新的Token返回给客户端,并且把这个Token缓存到Redis里面,这种操作成为刷新Token过期时间。

二、客户端如何更新令牌?

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

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

三、如何在响应中添加令牌?

​ 我们定义OAuth2Filter类拦截所有的HTTP请求,一方面它会把请求中的Token字符串提取出来,封装成对象交给Shiro框架;另一方面,它会检查Token的有效性。如果Token过期,那么会生成新的Token,分别存储在ThreadLocalToken和Redis中。之所以要把新令牌保存到ThreadLocalToken里面,是因为要向AOP切面类传递这个新令牌。虽然OAuth2Filter中有doFilterInternal()方法,我们可以得到响应并且写入新令牌。但是这个做非常麻烦,首先我们要通过IO流读取响应中的数据,然后还要把数据解析成JSON对象,最后再放入这个新令牌。如果我们定义了AOP切面类,拦截所有Web方法返回的R对象,然后在R对象里面添加新令牌,这多简单啊。但是OAuth2Filter和AOP切面类之间没有调用关系,所以我们难把新令牌传给AOP切面类。这里我想到了ThreadLocal,只要是同一个线程,往ThreadLocal里面写入数据和读取数据是完全相同的。在Web项目中,从OAuth2Filter到AOP切面类,都是由同一个线程来执行的,中途不会更换线程。所以我们可以放心的把新令牌保存都在ThreadLocal里面,AOP切面类可以成功的取出新令牌,然后往R对象里面添加新令牌即可。ThreadLocalToken是自定义的类,里面包含了ThreadLocal类型的变量,可以用来保存线程安全的数据,而且避免了使用线程锁。

四、创建ThreadLocalToken类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.emos.wx.config.shiro;

import org.springframework.stereotype.Component;

@Component
public class ThreadLocalToken {

private ThreadLocal<String> local=new ThreadLocal<>();

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

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


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

}

伍、创建过滤器

注意事项:因为在OAuth2Filter类中要读写ThreadLocal中的数据,所以OAuth2Filter类必须要设置成多例的,否则ThreadLocal将无法使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package com.example.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")
//设置多例否则向ThreadLocalToken存储会出问题
public class OAuth2Filter extends AuthenticatingFilter {
@Autowired
private ThreadLocalToken threadLocalToken;

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

@Autowired
private JwtUtil jwtUtil;

@Autowired
private RedisTemplate redisTemplate;

/**
* 拦截下后封装令牌字符串为令牌对象
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req= (HttpServletRequest) request;
String token=getRequestToken(req);
if(StrUtil.isBlank(token)){
return null;
}
return new OAuth2Token(token);
}

/**
* 拦截请求,判断请求是否需要被Shiro处理
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest req= (HttpServletRequest) request;
// Ajax提交application/json数据的时候,会先发出Options请求

// 这里要放行Options请求,不需要Shiro处理
if(req.getMethod().equals(RequestMethod.OPTIONS.name())){
return true;
}
// 除了Options请求之外,所有请求都要被Shiro处理
return false;
}

/**
* 该方法用于处理所有应该被Shiro处理的请求
* @param request
* @param response
* @return
* @throws Exception
*/
@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"));

threadLocalToken.clear();

String token=getRequestToken(req);
if(StrUtil.isBlank(token)){
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效的令牌");
return false;
}
// 检查令牌是否过期
try{
jwtUtil.verifierToken(token);
}catch (TokenExpiredException e){
// 客户端令牌过期,查询Redis中是否存在令牌,如果存在令牌就重新生成一个令牌给客户端
if(redisTemplate.hasKey(token)){
redisTemplate.delete(token);
int userId=jwtUtil.getUserId(token);
token=jwtUtil.createToken(userId);
redisTemplate.opsForValue().set(token,userId+"",cacheExpire, TimeUnit.DAYS);
// 把新令牌绑定到线程
threadLocalToken.setToken(token);
}
else{
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("令牌已过期");
return false;
}
}catch (Exception e){
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效的令牌");
return false;
}
// 间接调用realm类
boolean bool=executeLogin(request,response);
return bool;
}

/**
* 判定用户登录失败
* @param token
* @param e
* @param request
* @param response
* @return
*/
@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"));
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
try{
resp.getWriter().print(e.getMessage());
}catch (Exception exception){

}

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"));
super.doFilterInternal(request, response, chain);

}

/**
* 获取请求中的令牌
* @param request
* @return
*/
private String getRequestToken(HttpServletRequest request){
String token=request.getHeader("token");
if(StrUtil.isBlank(token)){
token=request.getParameter("token");
}
return token;
}
}

六、创建ShiroConfig类

我们要创建的ShiroConfig类,是用来把OAuth2Filter和OAuth2Realm配置到Shiro框架,这样我们辛苦搭建的Shiro+JWT才算生效。

设置配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.example.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 {

@Bean("securityManager")
public SecurityManager securityManager(OAuth2Realm realm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setRememberMeManager(null);
return securityManager;
}

@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);
shiroFilter.setFilters(map);

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");
// 除了上面的请求都需要认证
filterMap.put("/**", "oauth2");

shiroFilter.setFilterChainDefinitionMap(filterMap);

return shiroFilter;

}

@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}

七、创建AOP切面类

我们要在请求与响应中加入令牌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.example.emos.wx.aop;


import com.example.emos.wx.common.util.R;
import com.example.emos.wx.config.shiro.ThreadLocalToken;
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;

@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 joinPoint) throws Throwable {

R r = (R)joinPoint.proceed();

String token = threadLocalToken.getToken();

if (token != null) {
r.put("token", token);
threadLocalToken.clear();
}

return r;


}

}

八、返回给客户端的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.example.emos.wx.config;

import com.example.emos.wx.exception.EmosException;
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.*;

@RestControllerAdvice
@Slf4j
public class ExceptionAdvice {
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
// 捕获全局异常
public String validExceptionHandler(Exception e){

log.info("执行异常");

if(e instanceof MethodArgumentNotValidException){
MethodArgumentNotValidException exception= (MethodArgumentNotValidException) e;
return exception.getBindingResult().getFieldError().getDefaultMessage();
}
else if(e instanceof EmosException){
EmosException exception= (EmosException) e;
return exception.getMsg();
}
else if(e instanceof UnauthorizedException){
return "你不具备相关权限";
}
else{
return "后端执行异常";
}


}



}

执行顺序

一、为什么XSSFilter最先执行?

​ Emos系统接收到HTTP请求之后,首先由XSSFilter来处理请求。因为XSSFilter是标准的Servlet过滤器,所以他执行的优先级要高于ShiroFilter和AOP拦截器的。这也很好理解,还没轮到Controller中的Web方法执行,AOP连接器自然不能运行。另外,XSSFilter使用@WebFilter注解定义出来的过滤器,所以他的优先级比SpringMVC中注册的Filter优先级更高,所以XSSFilter早于SpringMVC执行。这个也能说得通,我们希望先把请求中的数据先转义,然后再由SpringMVC框架来处理请求。

二、OAuth2Filter的执行

​ 因为OAuth2Filter是在SpringMVC中注册的Filter,所以它晚于Servlet过滤器的执行。但是SpringMVC中注册过滤器有个好处,就是可以规定Filter的优先级别,所以定义普通的Filter,注册在SpringMVC上更加的妥当。

​ 我们在定义OAuth2Filter的时候,声明了很多的方法,但是在注册流程中,我们只能看到doFilterInternal()方法的执行,这又是为什么呢?

​ 我们声明Shiro过滤器拦截路径的时候,为登陆和注册路径下的请求,设置了放行,所以验证与授权并没有生效。等我们将来写具体的业务类型的Web方法,添加相关的Shiro注解,这时候OAuth2Filter中的其他方法就得以运行了。

三、TokenAspect的作用

​ TokenAspect是切面类,拦截所有Web方法的返回值。TokenAspect先检测ThreadLocalToken中有没有令牌字符串?如果有就把刷新后的令牌写入Web方法返回的R对象里面。因此说,Web方法每次执行的时候,TokenAspect都会随之运行,这在正常不过了。