登录校验
在java单体架构项目中,可以采用JWT令牌方式结合spring MVC拦截器的方式进行登录校验及用户信息传递,但在微服务场景下,每个服务独立部署,如果每个服务均做登录校验,需要:
- 每个微服务都需要知道JWT的秘钥,不安全
- 每个微服务重复编写登录校验代码、权限校验代码,麻烦
由于网关是所有微服务的入口,所有请求都需要先经过网关,因此可以将登录校验的工作放到网关中完成。只需编写一个自定义 GlobalFilter
,在其中完成 JWT 登录校验即可。需要注意的是,GlobalFilter
是网关的过滤器,它会在 Spring MVC 的拦截器之前生效。
用户信息传递
在微服务架构中,由于多台服务器无法共享同一个 ThreadLocal
,因此不能直接将用户信息存入 ThreadLocal
供其他业务使用。解决方案如下:
- 在网关的
GlobalFilter
中将用户信息保存到请求头中,并转发到下游微服务。
- 下游微服务需要设置一个 MVC 拦截器,用于拦截请求并从请求头中取出相关信息。
整体流程图如下:

参考代码如下:
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) { ServerHttpRequest request = exchange.getRequest(); if(isExclue(request.getPath().toString())){ return chain.filter(exchange); } String token = null; List<String> headers = request.getHeaders().get("authorization"); if(headers != null && !headers.isEmpty()){ token = headers.get(0); } Long userId = null; try{ userId = jwtTool.parseToken(token); } catch (UnauthorizedException e){ ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } 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; } }
|
最佳实践是将用户信息传递拦截器和登录校验拦截器分离,否则如果二者耦合在一起,就只能处理两种情况:
- 业务需要用户登录,可以获取登录用户信息;
- 业务不需要用户登录,但该业务无法获取登录用户信息;
如果一个业务需要允许未登录用户访问,同时需要获取登录用户的信息,就必须将两个拦截器分离。代码如下:
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 { String authorization = request.getHeader(JwtConstants.USER_HEADER); if (authorization == null) { return true; } 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 { Long userId = UserContext.getUser(); if (userId == null) { response.setStatus(401); response.sendError(401, "未登录用户无法访问!"); return false; } return true; } }
|
__END__