单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。
1. 思路分析
既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做。
不过,这里存在几个问题:
(1)网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?
可以使用网关过滤器。
(2)网关校验JWT之后,如何将用户信息传递给微服务?
由于网关发送请求到微服务依然采用的是Http
请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。
(3)微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?
在微服务发起调用时把用户信息存入请求头。
2. 登录校验过滤器
白名单的配置:
在 application.yml
或 application.properties
文件中配置不需要拦截的路径白名单。
gateway:
ignoreUrls:
- /auth/login
- /auth/register
- /public/**
在这个示例中,/auth/login
、/auth/register
、/public/**
等路径将不需要进行登录校验。
java
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
@Override
public 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校验并解析token
Long 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
}
@Override
public int getOrder() {
return 0; // 设置过滤器优先级,值越小优先级越高
}
}
3. 保存用户到请求头
5.6.部分代码可以参照下面修改:
java
// 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:
java
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
// 定义一个 Feign 的 RequestInterceptor Bean,用于在请求发出前执行自定义拦截操作
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取当前登录用户的ID
// UserContext 是一个上下文工具类,用于存储和获取当前线程的用户信息
Long userId = UserContext.getUser();
// 检查用户ID是否为 null,如果为 null 则表示用户未登录或无法获取用户信息
if(userId == null) {
// 如果用户ID为空,不做任何处理,直接返回,跳过拦截器逻辑
return;
}
// 如果用户ID不为空,将用户ID作为请求头添加到 Feign 请求中
// "user-info" 是请求头的键,下游微服务可以从该请求头中获取用户ID
template.header("user-info", userId.toString());
}
};
}
5. 总结
思路分析中提到的三个问题,目前已经全部解决。
接下来,微服务需要用户信息,只需要编写拦截器,获取用户信息并保存到ThreadLocal中,然后放行即可。
由于每个微服务都有获取登录用户的需求,因此拦截器可以直接写在common公共服务
中,并写好自动装配。这样微服务只需要引入common
就可以直接具备拦截器功能,无需重复编写。