微服务安全上下文的透明传递------ThreadLocal透传与HTTP头转发的完整链路
网关鉴权之后,下游的每个微服务都需要知道"当前请求是谁"。如果不做处理,每个Controller都要从Header里解析一遍用户信息,每个RestTemplate调用都要把用户信息塞回Header。这套方案用 ThreadLocal + 拦截器 + Hystrix策略 三条线实现了安全上下文的零感知传递------业务代码一行不改,在任何地方都能拿到当前用户,跨服务调用自动携带。
文章目录
- 微服务安全上下文的透明传递------ThreadLocal透传与HTTP头转发的完整链路
-
- 一、问题:用户信息在微服务调用链中怎么传递
- 二、ThreadLocal存储------一次请求内全局可用
- [三、入站------从HTTP Header提取用户信息](#三、入站——从HTTP Header提取用户信息)
- 四、出站------RestTemplate自动携带用户信息
- 五、跨线程池------Hystrix隔离线程时ThreadLocal的丢失与恢复
-
- [5.1 Hystrix插件重置------替换默认并发策略](#5.1 Hystrix插件重置——替换默认并发策略)
- [5.2 自定义并发策略------包装Callable](#5.2 自定义并发策略——包装Callable)
- [5.3 代理Callable------恢复上下文](#5.3 代理Callable——恢复上下文)
- 六、完整链路总结
- 七、结语
一、问题:用户信息在微服务调用链中怎么传递
用户请求 → Gateway(鉴权) → Service A → RestTemplate → Service B → Service C
每层都可能需要知道"是谁在访问"------审计日志、数据权限、操作记录都需要当前用户ID。三个问题:
- 入站:Gateway已经把用户信息鉴权过了,下游服务不需要再鉴权,但怎么拿到"这个人是谁"
- 出站:Service A调Service B时,怎么把用户信息从当前请求传递到下一个请求
- 线程池:Hystrix用线程池隔离,ThreadLocal里的信息过不了线程池边界
三条线各自解决一个问题,合起来就是完整的方案。
二、ThreadLocal存储------一次请求内全局可用
java
package com.verycloud.platform.common.security.data;
public class CurrentLoginUserHolder {
private static ThreadLocal<CurrentLoginUser> loginUser = new ThreadLocal<>();
public static CurrentLoginUser getCurrentLoginUser() {
return loginUser.get();
}
public static void setCurrentLoginUser(CurrentLoginUser currentLoginUser) {
loginUser.set(currentLoginUser);
}
public static void removeCurrentLoginUser() {
loginUser.remove();
}
}
ThreadLocal 保证每个请求线程有自己独立的用户信息副本------请求A改不到请求B的用户。任何地方调 CurrentLoginUserHolder.getCurrentLoginUser() 都能拿到当前用户,不需要从Controller层层层传参到Service到DAO。
remove() 是必须的------Tomcat线程池复用线程,上一次请求的用户信息残留在ThreadLocal里,下一个请求复用同一线程时读到的是上次的人。
三、入站------从HTTP Header提取用户信息
java
package com.verycloud.microservice.settings.interceptor;
public class WrapApiResultInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
String json = request.getHeader("CUSTOM-REQUEST-HEADER");
if (!StringUtils.isEmpty(json)) {
json = URLDecoder.decode(json, "UTF-8");
try {
ObjectMapper objectMapper = new ObjectMapper();
CurrentLoginUser loginUser = objectMapper.readValue(
json, CurrentLoginUser.class);
if (loginUser != null) {
CurrentLoginUserHolder.setCurrentLoginUser(loginUser);
}
} catch (Throwable e) {
logger.error("反序列化失败", e);
}
}
return true; // 不拦截,即使无token也放行
}
}
preHandle 拦截所有进入当前服务的HTTP请求,从Header CUSTOM-REQUEST-HEADER 读JSON字符串,反序列化为 CurrentLoginUser 对象,设入 ThreadLocal。
注意:即使Header为空也不拦截(return true)------不是所有的接口都需要鉴权,比如健康检查、公开接口。拿不到用户信息就放行,由业务逻辑自己判断是否需要登录态。
四、出站------RestTemplate自动携带用户信息
java
package com.verycloud.microservice.settings.interceptor;
@Configuration
public class MyRestTemplateInterceptor {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
builder.additionalInterceptors(requestInterceptor());
return builder.build();
}
@Bean
public ClientHttpRequestInterceptor requestInterceptor() {
return (request, body, execution) -> {
CurrentLoginUser currentLoginUser =
CurrentLoginUserHolder.getCurrentLoginUser();
if (currentLoginUser != null) {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(currentLoginUser);
request.getHeaders().add("CUSTOM-REQUEST-HEADER", json);
}
return execution.execute(request, body);
};
}
}
RestTemplate 通过 additionalInterceptors() 注入自定义拦截器。每次发HTTP请求前,从 ThreadLocal 读取当前用户,序列化为JSON,塞进 CUSTOM-REQUEST-HEADER 请求头。
下游服务的 WrapApiResultInterceptor 收到这个Header,反序列化回 CurrentLoginUser,设入自己的 ThreadLocal。从Service A到Service B的用户信息传递是自动的------调用方不需要显式传参,被调用方不需要额外鉴权。
五、跨线程池------Hystrix隔离线程时ThreadLocal的丢失与恢复
上面两条方案在同步调用时工作良好。但当Hystrix用线程池隔离时,业务逻辑跑在另一个线程里------ThreadLocal不会自动从主线程传递到Hystrix线程。
Hystrix允许自定义 HystrixConcurrencyStrategy 来干预线程池的行为,关键方法是 wrapCallable():
5.1 Hystrix插件重置------替换默认并发策略
java
package com.verycloud.platform.utils.hystrix;
@Configuration
public class ThreadLocalConfiguration {
@Autowired(required = false)
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
@PostConstruct
public void init() {
// 备份Hystrix已有的所有插件
HystrixEventNotifier eventNotifier =
HystrixPlugins.getInstance().getEventNotifier();
HystrixMetricsPublisher metricsPublisher =
HystrixPlugins.getInstance().getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy =
HystrixPlugins.getInstance().getPropertiesStrategy();
HystrixCommandExecutionHook commandExecutionHook =
HystrixPlugins.getInstance().getCommandExecutionHook();
HystrixPlugins.reset();
// 注入自定义并发策略(保留已有插件不变)
HystrixPlugins.getInstance().registerConcurrencyStrategy(
new ThreadLocalAwareStrategy(existingConcurrencyStrategy));
HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
}
}
关键步骤:先 reset() 清空所有已注册插件,再逐一重新注册。唯一替换的是 HystrixConcurrencyStrategy------用自定义的 ThreadLocalAwareStrategy 替换默认的。其余的 eventNotifier、metricsPublisher、propertiesStrategy、commandExecutionHook 保持原样。
5.2 自定义并发策略------包装Callable
java
package com.verycloud.platform.utils.hystrix;
public class ThreadLocalAwareStrategy extends HystrixConcurrencyStrategy {
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
public ThreadLocalAwareStrategy(
HystrixConcurrencyStrategy existingConcurrencyStrategy) {
this.existingConcurrencyStrategy = existingConcurrencyStrategy;
}
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.wrapCallable(
new DelegatingUserContextCallable<T>(callable,
CurrentLoginUserHolder.getCurrentLoginUser()))
: super.wrapCallable(
new DelegatingUserContextCallable<T>(callable,
CurrentLoginUserHolder.getCurrentLoginUser()));
}
}
wrapCallable() 在Hystrix把任务提交到线程池之前被调用。这里劫持了传入的 Callable------把它包进 DelegatingUserContextCallable 里。在当前线程(主线程)尚未离开这个调用点之前,把ThreadLocal里的用户对象快照下来,传给真正执行的线程。
5.3 代理Callable------恢复上下文
java
package com.verycloud.platform.utils.hystrix;
public class DelegatingUserContextCallable<V> implements Callable<V> {
private Callable<V> delegate;
private CurrentLoginUser originalUserInfo;
public DelegatingUserContextCallable(Callable<V> delegate,
CurrentLoginUser userInfo) {
this.delegate = delegate;
this.originalUserInfo = userInfo;
}
@Override
public V call() throws Exception {
// 在新线程中恢复用户上下文
CurrentLoginUserHolder.setCurrentLoginUser(originalUserInfo);
try {
return delegate.call();
} finally {
this.originalUserInfo = null;
}
}
}
call() 方法在Hystrix的新线程中被执行。第一行就把构造时传入的用户信息恢复到新线程的ThreadLocal里------从这一刻起,业务代码在新线程里调 CurrentLoginUserHolder.getCurrentLoginUser() 能拿到正确的人。
finally 里把引用置空,帮助GC回收。
六、完整链路总结
① 网关认证
└── 鉴权完成后,将 CurrentLoginUser 序列化为JSON
└── 写入 HTTP Header: CUSTOM-REQUEST-HEADER
② Service A 入站
└── WrapApiResultInterceptor.preHandle()
└── 读 Header → 反序列化 → setCurrentLoginUser()
③ Service A 业务代码
└── 任意层调 CurrentLoginUserHolder.getCurrentLoginUser()
└── ThreadLocal 直接返回
④ 如果用了 Hystrix 线程池
└── ThreadLocalAwareStrategy.wrapCallable()
└── 主线程快照 CurrentLoginUser
└── DelegatingUserContextCallable.call()
└── 新线程 restore CurrentLoginUser → 执行 → finally 清理
⑤ Service A 出站(调Service B)
└── MyRestTemplateInterceptor
└── 读 ThreadLocal → 序列化 → 写入 CUSTOM-REQUEST-HEADER
⑥ Service B 入站
└── 同② → 循环继续
三条线各司其职:
| 线 | 组件 | 解决的问题 |
|---|---|---|
| 入站 | WrapApiResultInterceptor |
HTTP Header → ThreadLocal |
| 出站 | MyRestTemplateInterceptor |
ThreadLocal → HTTP Header |
| 线程池穿透 | ThreadLocalAwareStrategy + DelegatingUserContextCallable |
ThreadLocal不会被线程池吃掉 |
七、结语
这套方案的核心原则:业务代码永远不感知用户信息的传递机制 。任何一个Service、任何一个Mapper、任何一个工具类------只要调 CurrentLoginUserHolder.getCurrentLoginUser() 就能拿到当前用户。不需要从Controller层层传参,不需要在Feign接口上声明 @RequestHeader,不需要在Hystrix代码里手动传ThreadLocal。
三条线覆盖了同步传递、异步穿透、服务间转发三个场景。Hystrix插件的替换是整条方案里最容易被忽略的一环------定时任务、异步处理如果用线程池隔离但没有这份策略,跑在子线程里的代码拿不到用户信息,审计日志就丢了关键字段。