登录校验

在java单体架构项目中,可以采用JWT令牌方式结合spring MVC拦截器的方式进行登录校验及用户信息传递,但在微服务场景下,每个服务独立部署,如果每个服务均做登录校验,需要:

  • 每个微服务都需要知道JWT的秘钥,不安全
  • 每个微服务重复编写登录校验代码、权限校验代码,麻烦

由于网关是所有微服务的入口,所有请求都需要先经过网关,因此可以将登录校验的工作放到网关中完成。只需编写一个自定义 GlobalFilter,在其中完成 JWT 登录校验即可。需要注意的是,GlobalFilter 是网关的过滤器,它会在 Spring MVC 的拦截器之前生效。

用户信息传递

网关过滤器添加Header

在微服务架构中,由于多台服务器无法共享同一个 ThreadLocal,因此不能直接将用户信息存入 ThreadLocal 供其他业务使用。解决方案如下:

  1. 在网关的 GlobalFilter 中将用户信息保存到请求头中,并转发到下游微服务。
  2. 下游微服务需要设置一个 MVC 拦截器,用于拦截请求并从请求头中取出相关信息。

整体流程图如下:

image-20250202213717589

参考代码如下:

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
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

private final AuthProperties authProperties;
private final JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取request
ServerHttpRequest request = exchange.getRequest();
// 2.判断是否需要登录拦截
if(isExclue(request.getPath().toString())){
// 放行
return chain.filter(exchange);
}
// 3.获取token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if(headers != null && !headers.isEmpty()){
token = headers.get(0);
}
// 4.校验并解析token
Long userId = null;
try{
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e){
// 拦截
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 5.传递用户信息
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info", userInfo))
.build();
return chain.filter(swe);
}

// 判断是否需要登录拦截,返回值为否拦截
private boolean isExclue(String path) {
for(String pathPattern: authProperties.getExcludePaths()){
if(antPathMatcher.match(pathPattern, path)) return true;
}
return false;
}

// 过滤器优先级,越小越优先
@Override
public int getOrder() {
return 0;
}
}
MVC拦截器获取Header

最佳实践是将用户信息传递拦截器和登录校验拦截器分离,否则如果二者耦合在一起,就只能处理两种情况:

  1. 业务需要用户登录,可以获取登录用户信息;
  2. 业务不需要用户登录,但该业务无法获取登录用户信息;

如果一个业务需要允许未登录用户访问,同时需要获取登录用户的信息,就必须将两个拦截器分离。代码如下:

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
@Slf4j
public class UserInfoInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.尝试获取头信息中的用户信息
String authorization = request.getHeader(JwtConstants.USER_HEADER);
// 2.判断是否为空
if (authorization == null) {
return true;
}
// 3.转为用户id并保存
try {
Long userId = Long.valueOf(authorization);
UserContext.setUser(userId);
return true;
} catch (NumberFormatException e) {
log.error("用户身份信息格式不正确,{}, 原因:{}", authorization, e.getMessage());
return true;
}
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理用户信息
UserContext.removeUser();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
public class LoginAuthInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.尝试获取用户信息
Long userId = UserContext.getUser();
// 2.判断是否登录
if (userId == null) {
response.setStatus(401);
response.sendError(401, "未登录用户无法访问!");
// 2.3.未登录,直接拦截
return false;
}
// 3.登录则放行
return true;
}
}

__END__