前言:
在单体架构的项目中,实现对用户的登录校验以及上下文的获取轻而易举,但是在微服务架构中,由于不同服务之间相互独立,不能直接获取,本文就介绍了通过GateWay网关来实现上述功能。
网关
在微服务架构中,Gateway(网关) 是整个系统的“流量总入口”。本文采用Spring Cloud Gateway,它底层基于 Netty 和 WebFlux 响应式编程,具有如下特点:
- 高并发:能够以极低的资源消耗处理海量连接。
- 非阻塞:它是异步非阻塞模型,非常适合作为微服务的“安检口”。
GateWay拦截(登录校验)
在网关中统一拦截所有需要拦截的请求,解析JWT令牌并将私有声明写入请求头。
在GateWay模块中编写AuthGlobalFilter,拦截器需要实现GlobalFilter和Ordered接口。
@Component
@RequiredArgsConstructor
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) {
// 获取requset
ServerHttpRequest request = exchange.getRequest();
// 判断是否需要拦截
if (isInclude(request.getPath().toString())){
return chain.filter(exchange);
}
// 获取token
String token = null;
List<String> authorization = request.getHeaders().get("authorization");
if (authorization != null && !authorization.isEmpty()){
token = authorization.get(0);
}
// 解析校验token
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 serverWebExchange = exchange.mutate()
.request(builder -> builder.header("user-info", userInfo))
.build();
// 放行
return chain.filter(serverWebExchange);
}
private boolean isInclude(String path) {
for (String excludePath : authProperties.getExcludePaths()) {
if (antPathMatcher.match(excludePath, path)){
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
该拦截器不使用Servlet规范,因此使用的API与以往有较大差别,获取请求信息需要调用exchange的getRequest()方法,通过拦截是返回chain.filter(exchange),将请求交给下个拦截器。
重点:
- 传递用户信息
由于网关异步非阻塞地处理请求,一个请求的执行过程可能会在多个不同的线程之间切换,因此不能在网关直接使用ThreadLocal。本文将需要传递的信息写入请求头中,由MVC将信息写入ThreadLocal。 - 拦截顺序
实现 Ordered 接口并将返回值设为 0,是为了确保我们的身份校验在负载均衡之前执行,尽早拦截非法请求,节省后端资源
代码解读:
为了保证线程安全和异步的高效,ServerWebExchange 对象一旦创建就是不可变的。
如果你想给下个微服务增加一个 user-info 的 Header,你不能直接调用 request.addHeader(...)(根本没有这个方法)。
解决方案:必须调用 mutate()(变形/突变)。这就像是给原始对象拍了个“快照”,然后基于快照克隆出一个带有新 Header 的新对象,再把这个新对象传给下游(MVC拦截器)。
MVC拦截(解析上下文)
在微服务项目的共享模块中使用MVC拦截器将上下文存入ThreadLocal。
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userInfo = request.getHeader("user-info");
if (StrUtil.isNotBlank(userInfo)) {
UserContext.setUser(Long.parseLong(userInfo));
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
UserContext.removeUser();
}
}
注册拦截器
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
必须在 afterCompletion 中清理 ThreadLocal。因为 Tomcat 线程池会复用线程,如果这次请求不清理,下一个‘倒霉’的匿名用户可能会带上上一个用户的 ID,造成严重的业务 BUG。
@ConditionalOnClass注解作用:
只有当当前项目的类路径(Classpath)中存在 DispatcherServlet 这个类时,这个配置类(MvcConfig)才会生效。
- 对于普通业务微服务: 它们引入了 spring-boot-starter-web,底层基于 Servlet 容器,类路径下一定有 DispatcherServlet。此时,MvcConfig 生效,注册 UserInfoInterceptor 拦截器,从而实现用户上下文的自动获取。
- 对于网关服务: Spring Cloud Gateway 是基于 WebFlux(响应式编程)构建的,它不依赖 Servlet 容器,因此类路径下没有 DispatcherServlet。 如果没有这个注解,网关服务在启动时会因为尝试加载基于 Servlet 的 WebMvcConfigurer 而报错。有了这个注解,网关会自动跳过这个配置,避免冲突。
Feign拦截(服务互通)
不同微服务运行在不同的JVM上,微服务之间互相调用时就无法使用ThreadLocal。因此我们在Feign客户端中添加一次拦截,将信息像网关那样写入请求头,再次由MVC解析出信息。
拦截器编写:
public class config {
/**
拦截器,用于在请求头中添加用户信息
*/
@Bean
public RequestInterceptor userInfoInterceptor() {
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate templateTemplate) {
Long userId = UserContext.getUser();
if (userId != null){
templateTemplate.header("user-info", userId.toString());
}
}
};
}
}
由于逻辑较为简单,此处直接使用了匿名内部类实现。
拦截器的应用:
在每个业务模块导入该模块,并在启动类上的
@EnableFeignClients(basePackages = "com.hmall.api.client")
注解中添加该配置
@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = config.class)
总结
ThreadLocal:微服务里的‘接力棒’而非‘马拉松’
很多人以为 ThreadLocal 能从网关一直透传到最后一个微服务,这是不可能的。ThreadLocal 的特权仅限于 同一进程的同一线程。
在微服务漫长的链路上,我们靠的是 “Header 传值 + 拦截器接力”。
- 网关负责起始发射(JWT -> Header)。
- 各微服务拦截器负责中途接应(Header -> ThreadLocal)。
- Feign 拦截器负责二次接力(ThreadLocal -> Header)。