微服务中如何获取用户信息

微服务如何获取用户信息

JWT 在微服务中的基本传递流程

  1. 客户端发送请求到网关
  • 用户通过登录接口获取 JWT。

  • 登录成功后,客户端在后续请求中将 JWT 放入请求头中,通常格式如下:

1
Authorization: Bearer <jwt-token>
  1. 网关验证 JWT 并转发请求
  • 网关解析 Authorization 头中的 JWT。

  • 验证 JWT 是否有效(签名校验、过期时间校验等)。

  • 如果 JWT 有效,将请求转发到相应的微服务,同时可以选择将 JWT 转发到目标微服务(一般会保留在请求头中)。

  1. 微服务验证 JWT
  • 微服务接收到网关的转发请求后,可以再次验证 JWT,确保请求的合法性。

  • 根据 JWT 中的用户信息(如用户 ID、角色等)执行具体业务逻辑。

  1. 微服务之间的请求
  • 当一个微服务需要调用另一个微服务时,通常会将用户的 JWT 附加到新的请求头中,传递到下一个微服务,确保链路中的身份信息一致。

JWT 在网关到微服务的传递

1. 客户端到网关

  • 客户端行为:
    • 客户端(如浏览器、移动应用、Postman 等)在登录成功后保存从网关获取的 JWT(例如在本地存储中)。
    • 在后续请求中,客户端将 JWT 放入 HTTP 请求头中。

请求示例:

1
2
3
GET /api/user/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer <jwt-token>

网关行为

  • 网关接收请求后,验证 JWT:

  • 使用公钥或秘钥验证 JWT 的签名。

  • 检查 JWT 是否过期。

  • 验证通过后,可以选择将 JWT 保留并转发到目标微服务。

登录校验的过滤器示例如下:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

// 注入 JwtTool,用于解析和验证 JWT
private final JwtTool jwtTool;

// 注入 AuthProperties,用于获取无需拦截的路径配置
private final AuthProperties authProperties;

// 路径匹配器,用于判断请求路径是否匹配配置的排除路径
private final AntPathMatcher antPathMatcher = new AntPathMatcher();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取当前的 HTTP 请求对象
ServerHttpRequest request = exchange.getRequest();

// 2.判断请求路径是否在排除路径中(不需要拦截)
if (isExclude(request.getPath().toString())) {
// 如果路径在排除列表中,直接放行请求
return chain.filter(exchange);
}

// 3.从请求头中获取 token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (!CollUtils.isEmpty(headers)) {
// 获取 Authorization 头的第一个值
token = headers.get(0);
}

// 4.校验并解析 token
Long userId = null; // 用于存储解析出的用户 ID
try {
// 调用 JwtTool 解析 token,如果失败会抛出异常
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 如果 token 无效或者解析失败,则返回 401 未授权错误
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}

// TODO 5.如果 token 有效,可以将用户信息传递给后续微服务
// 在这里可以选择将用户信息(例如 userId)放入请求头或请求属性中
System.out.println("userId = " + userId); // 打印解析出的用户 ID(调试用)

// 6.如果校验成功,放行请求
return chain.filter(exchange);
}

/**
* 判断请求路径是否在排除路径列表中
* @param antPath 当前请求路径
* @return 如果匹配排除路径列表中的某一项,则返回 true,否则返回 false
*/
private boolean isExclude(String antPath) {
for (String pathPattern : authProperties.getExcludePaths()) {
// 使用 AntPathMatcher 判断路径是否匹配
if (antPathMatcher.match(pathPattern, antPath)) {
return true; // 匹配成功,说明该路径无需拦截
}
}
return false; // 如果没有匹配任何排除路径,则需要拦截
}

/**
* 设置过滤器的执行顺序
* @return 数字越小,优先级越高
*/
@Override
public int getOrder() {
return 0;
}
}

2. 网关到微服务

由于网关发送请求到微服务依然采用的是Http请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。

网关将用户信息存入请求头内:
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
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取 request 对象
ServerHttpRequest request = exchange.getRequest();

// 2. 判断是否需要登录拦截
if (isExclude(request.getPath().toString())) {
// 如果路径在排除列表中,直接放行
return chain.filter(exchange);
}

// 3. 获取请求头中的 token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (headers != null && !headers.isEmpty()) {
token = headers.get(0); // 获取第一个 Authorization 头
}

// 4. 校验并解析 token
Long userId = null;
try {
userId = jwtTool.parseToken(token); // 调用 JWT 工具解析 token
} catch (UnauthorizedException e) {
// 如果 token 校验失败,返回 401 未授权
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}

// 5. 传递用户信息
String userInfo = userId.toString();
ServerWebExchange ex = exchange.mutate()
.request(b -> b.header("user-info", userInfo)) // 将用户信息添加到请求头
.build();

// 6. 放行请求
return chain.filter(ex);
}
拦截器获取用户信息

在common中写一个用于保存登录用户的ThreadLocal工具:

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
public class UserContext {

// 定义 ThreadLocal 变量,用于存储当前线程中的用户 ID
private static final ThreadLocal<Long> tl = new ThreadLocal<>();

/**
* 保存当前登录用户信息到 ThreadLocal
* @param userId 用户 ID
*/
public static void setUser(Long userId) {
tl.set(userId);
}

/**
* 获取当前登录用户信息
* @return 用户 ID
*/
public static Long getUser() {
return tl.get();
}

/**
* 移除当前登录用户信息
*/
public static void removeUser() {
tl.remove();
}
}

接下来,我们需要编写拦截器,获取用户信息并保存到UserContext,然后放行即可。

由于每个微服务都有获取登录用户的需求,因此拦截器我们直接写在common中,并写好自动装配。这样微服务只需要引入common就可以直接具备拦截器功能,无需重复编写。

我们在common模块下定义一个拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的用户信息
String userInfo = request.getHeader("user-info");
// 2.判断是否为空
if (StrUtil.isNotBlank(userInfo)) {
// 不为空,保存到ThreadLocal
UserContext.setUser(Long.valueOf(userInfo));
}
// 3.放行
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserContext.removeUser();
}
}

接着在common模块下编写SpringMVC的配置类,配置登录拦截器:

1
2
3
4
5
6
7
8
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}

不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是com.xxx.common.config,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。

基于SpringBoot的自动装配原理,我们要将其添加到resources目录下的META-INF/spring.factories文件中:

1
2
3
# 配置 Spring Boot 自动装配类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxx.common.config.MvcConfig

3. 微服务之间传递

微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?

这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor

1
2
3
4
5
6
7
8
public interface RequestInterceptor {

/**
* Called for every request.
* Add data using methods on the supplied {@link RequestTemplate}.
*/
void apply(RequestTemplate template);
}

我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。

由于FeignClient全部都是在api模块,因此我们在api模块的com.xxx.api.config.DefaultFeignConfig中编写这个拦截器:

com.xxx.api.config.DefaultFeignConfig中添加一个Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取登录用户
Long userId = UserContext.getUser();
if(userId == null) {
// 如果为空则直接跳过
return;
}
// 如果不为空则放入请求头中,传递给下游微服务
template.header("user-info", userId.toString());
}
};
}

好了,现在微服务之间通过OpenFeign调用时也会传递登录用户信息了。