八股八股微服务中如何获取用户信息
Jaron微服务如何获取用户信息
JWT 在微服务中的基本传递流程
- 客户端发送请求到网关:
1
| Authorization: Bearer <jwt-token>
|
- 网关验证 JWT 并转发请求:
网关解析 Authorization 头中的 JWT。
验证 JWT 是否有效(签名校验、过期时间校验等)。
如果 JWT 有效,将请求转发到相应的微服务,同时可以选择将 JWT 转发到目标微服务(一般会保留在请求头中)。
- 微服务验证 JWT:
- 微服务之间的请求:
- 当一个微服务需要调用另一个微服务时,通常会将用户的 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>
|
网关行为:
登录校验的过滤器示例如下:
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 {
private final JwtTool jwtTool;
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest();
if (isExclude(request.getPath().toString())) { return chain.filter(exchange); }
String token = null; List<String> headers = request.getHeaders().get("authorization"); if (!CollUtils.isEmpty(headers)) { token = headers.get(0); }
Long userId = null; try { userId = jwtTool.parseToken(token); } catch (UnauthorizedException e) { ServerHttpResponse response = exchange.getResponse(); response.setRawStatusCode(401); return response.setComplete(); }
System.out.println("userId = " + userId);
return chain.filter(exchange); }
private boolean isExclude(String antPath) { for (String pathPattern : authProperties.getExcludePaths()) { if (antPathMatcher.match(pathPattern, antPath)) { return true; } } return false; }
@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) { ServerHttpRequest request = exchange.getRequest(); if (isExclude(request.getPath().toString())) { return chain.filter(exchange); } String token = null; List<String> headers = request.getHeaders().get("authorization"); if (headers != null && !headers.isEmpty()) { token = headers.get(0); } Long userId = null; try { userId = jwtTool.parseToken(token); } catch (UnauthorizedException e) { ServerHttpResponse response = exchange.getResponse(); response.setRawStatusCode(401); return response.setComplete(); } String userInfo = userId.toString(); ServerWebExchange ex = exchange.mutate() .request(b -> b.header("user-info", userInfo)) .build(); 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 {
private static final ThreadLocal<Long> tl = new ThreadLocal<>();
public static void setUser(Long userId) { tl.set(userId); }
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 { String userInfo = request.getHeader("user-info"); if (StrUtil.isNotBlank(userInfo)) { UserContext.setUser(Long.valueOf(userInfo)); } 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
| 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 {
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调用时也会传递登录用户信息了。