单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。
1. 思路分析
既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做。
不过,这里存在几个问题:
(1)网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?
可以使用网关过滤器。
(2)网关校验JWT之后,如何将用户信息传递给微服务?
由于网关发送请求到微服务依然采用的是Http
请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。
(3)微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?
在微服务发起调用时把用户信息存入请求头。
2. 登录校验过滤器
白名单的配置:
在 application.yml
或 application.properties
文件中配置不需要拦截的路径白名单。
gateway:ignoreUrls:- /auth/login- /auth/register- /public/**
在这个示例中,/auth/login
、/auth/register
、/public/**
等路径将不需要进行登录校验。
package com.cyt.gateway.filter;@Component // 声明为Spring容器中的一个Bean
@RequiredArgsConstructor // 生成构造方法,注入final成员变量
@EnableConfigurationProperties(AuthProperties.class) // 启用配置类AuthProperties的属性注入
public class AuthGlobalFilter implements GlobalFilter, Ordered {// 从配置文件中注入白名单路径列表,用于存放不需要JWT校验的路径@Value("${gateway.ignoreUrls}")private List<String> ignoreUrls;private final JwtTool jwtTool; // JWT工具类,用于解析和校验JWT@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1. 获取Request对象,以访问请求的相关信息ServerHttpRequest request = exchange.getRequest();// 2. 判断请求路径是否在白名单中(无需JWT校验)if (isIgnoreUrl(request.getPath().toString())) {// 路径在白名单中,直接放行请求return chain.filter(exchange);}// 3. 获取请求头中的token(假设token放在"authorization"头部)String token = null;List<String> headers = request.getHeaders().get("authorization");if (!CollUtils.isEmpty(headers)) { // 如果请求头包含authorization字段token = headers.get(0); // 获取第一个token值}// 4. 使用jwtTool校验并解析tokenLong userId = null;try {userId = jwtTool.parseToken(token); // 解析并获取用户ID} catch (UnauthorizedException e) {// 如果token无效或解析失败,则返回401状态码并拦截请求ServerHttpResponse response = exchange.getResponse();response.setRawStatusCode(401);return response.setComplete(); // 返回空的响应结束处理}// TODO 5. 如果token有效,可在此处传递用户信息(如通过exchange.getAttributes())System.out.println("userId = " + userId); // 打印用户ID供调试// 6. 放行请求return chain.filter(exchange);}/*** 判断路径是否在白名单中** @param path 请求路径* @return 如果路径在白名单中返回true,否则返回false*/private boolean isIgnoreUrl(String path) {for (String ignoreUrl : ignoreUrls) {// 使用正则匹配白名单路径中的通配符 "**"if (path.matches(ignoreUrl.replace("**", ".*"))) {return true; // 路径匹配白名单中的某一项,返回true}}return false; // 没有匹配到任何白名单路径,返回false}@Overridepublic int getOrder() {return 0; // 设置过滤器优先级,值越小优先级越高}
}
3. 保存用户到请求头
5.6.部分代码可以参照下面修改:
// 5.传递用户信息
// 将解析出的用户ID转换为字符串,以便可以在请求头中传递
String userInfo = userId.toString();// 使用 exchange.mutate() 方法创建一个新的 ServerWebExchange 对象,
// 该对象将携带额外的请求头 "user-info",包含用户ID信息。
// .mutate() 方法用于复制当前请求并进行修改。
// 在这里,我们使用 request(builder -> builder.header(...)) 方式为请求添加一个新的头部。
ServerWebExchange modifiedExchange = exchange.mutate().request(builder -> builder.header("user-info", userInfo)) // 将 "user-info" 头设置为用户ID字符串.build(); // 完成新的 ServerWebExchange 对象的构建// 6.放行
// 使用修改后的 ServerWebExchange 对象继续执行过滤链。
// 这样,下游服务可以通过 "user-info" 请求头获取到用户ID信息。
return chain.filter(modifiedExchange);
4. OpenFeign传递用户
在微服务发起调用时把用户信息存入请求头。
如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?
这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor
我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate
类来添加请求头,将用户信息保存到请求头中。
可以在OpenFeign的配置类中添加一个Bean:
@Bean
public RequestInterceptor userInfoRequestInterceptor(){// 定义一个 Feign 的 RequestInterceptor Bean,用于在请求发出前执行自定义拦截操作return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {// 获取当前登录用户的ID// UserContext 是一个上下文工具类,用于存储和获取当前线程的用户信息Long userId = UserContext.getUser();// 检查用户ID是否为 null,如果为 null 则表示用户未登录或无法获取用户信息if(userId == null) {// 如果用户ID为空,不做任何处理,直接返回,跳过拦截器逻辑return;}// 如果用户ID不为空,将用户ID作为请求头添加到 Feign 请求中// "user-info" 是请求头的键,下游微服务可以从该请求头中获取用户IDtemplate.header("user-info", userId.toString());}};
}
5. 总结
思路分析中提到的三个问题,目前已经全部解决。
接下来,微服务需要用户信息,只需要编写拦截器,获取用户信息并保存到ThreadLocal中,然后放行即可。
由于每个微服务都有获取登录用户的需求,因此拦截器可以直接写在common公共服务
中,并写好自动装配。这样微服务只需要引入common
就可以直接具备拦截器功能,无需重复编写。