网关登录校验

0 阅读4分钟

前言:

在单体架构的项目中,实现对用户的登录校验以及上下文的获取轻而易举,但是在微服务架构中,由于不同服务之间相互独立,不能直接获取,本文就介绍了通过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),将请求交给下个拦截器。

重点

  1. 传递用户信息
    由于网关异步非阻塞地处理请求,一个请求的执行过程可能会在多个不同的线程之间切换,因此不能在网关直接使用ThreadLocal。本文将需要传递的信息写入请求头中,由MVC将信息写入ThreadLocal。
  2. 拦截顺序
    实现 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 传值 + 拦截器接力”。

  1. 网关负责起始发射(JWT -> Header)。
  2. 各微服务拦截器负责中途接应(Header -> ThreadLocal)。
  3. Feign 拦截器负责二次接力(ThreadLocal -> Header)。