微服务安全上下文的透明传递——ThreadLocal透传与HTTP头转发的完整链路 微服务安全上下文的透明传递——ThreadLocal透传与HTTP头转发的完整链路网关鉴权之后下游的每个微服务都需要知道当前请求是谁。如果不做处理每个Controller都要从Header里解析一遍用户信息每个RestTemplate调用都要把用户信息塞回Header。这套方案用 ThreadLocal 拦截器 Hystrix策略 三条线实现了安全上下文的零感知传递——业务代码一行不改在任何地方都能拿到当前用户跨服务调用自动携带。文章目录微服务安全上下文的透明传递——ThreadLocal透传与HTTP头转发的完整链路一、问题用户信息在微服务调用链中怎么传递二、ThreadLocal存储——一次请求内全局可用三、入站——从HTTP Header提取用户信息四、出站——RestTemplate自动携带用户信息五、跨线程池——Hystrix隔离线程时ThreadLocal的丢失与恢复5.1 Hystrix插件重置——替换默认并发策略5.2 自定义并发策略——包装Callable5.3 代理Callable——恢复上下文六、完整链路总结七、结语一、问题用户信息在微服务调用链中怎么传递用户请求 → Gateway(鉴权) → Service A → RestTemplate → Service B → Service C每层都可能需要知道是谁在访问——审计日志、数据权限、操作记录都需要当前用户ID。三个问题入站Gateway已经把用户信息鉴权过了下游服务不需要再鉴权但怎么拿到这个人是谁出站Service A调Service B时怎么把用户信息从当前请求传递到下一个请求线程池Hystrix用线程池隔离ThreadLocal里的信息过不了线程池边界三条线各自解决一个问题合起来就是完整的方案。二、ThreadLocal存储——一次请求内全局可用packagecom.verycloud.platform.common.security.data;publicclassCurrentLoginUserHolder{privatestaticThreadLocalCurrentLoginUserloginUsernewThreadLocal();publicstaticCurrentLoginUsergetCurrentLoginUser(){returnloginUser.get();}publicstaticvoidsetCurrentLoginUser(CurrentLoginUsercurrentLoginUser){loginUser.set(currentLoginUser);}publicstaticvoidremoveCurrentLoginUser(){loginUser.remove();}}ThreadLocal保证每个请求线程有自己独立的用户信息副本——请求A改不到请求B的用户。任何地方调CurrentLoginUserHolder.getCurrentLoginUser()都能拿到当前用户不需要从Controller层层层传参到Service到DAO。remove()是必须的——Tomcat线程池复用线程上一次请求的用户信息残留在ThreadLocal里下一个请求复用同一线程时读到的是上次的人。三、入站——从HTTP Header提取用户信息packagecom.verycloud.microservice.settings.interceptor;publicclassWrapApiResultInterceptorimplementsHandlerInterceptor{OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{Stringjsonrequest.getHeader(CUSTOM-REQUEST-HEADER);if(!StringUtils.isEmpty(json)){jsonURLDecoder.decode(json,UTF-8);try{ObjectMapperobjectMappernewObjectMapper();CurrentLoginUserloginUserobjectMapper.readValue(json,CurrentLoginUser.class);if(loginUser!null){CurrentLoginUserHolder.setCurrentLoginUser(loginUser);}}catch(Throwablee){logger.error(反序列化失败,e);}}returntrue;// 不拦截即使无token也放行}}preHandle拦截所有进入当前服务的HTTP请求从HeaderCUSTOM-REQUEST-HEADER读JSON字符串反序列化为CurrentLoginUser对象设入ThreadLocal。注意即使Header为空也不拦截return true——不是所有的接口都需要鉴权比如健康检查、公开接口。拿不到用户信息就放行由业务逻辑自己判断是否需要登录态。四、出站——RestTemplate自动携带用户信息packagecom.verycloud.microservice.settings.interceptor;ConfigurationpublicclassMyRestTemplateInterceptor{BeanpublicRestTemplaterestTemplate(RestTemplateBuilderbuilder){builder.additionalInterceptors(requestInterceptor());returnbuilder.build();}BeanpublicClientHttpRequestInterceptorrequestInterceptor(){return(request,body,execution)-{CurrentLoginUsercurrentLoginUserCurrentLoginUserHolder.getCurrentLoginUser();if(currentLoginUser!null){ObjectMapperobjectMappernewObjectMapper();StringjsonobjectMapper.writeValueAsString(currentLoginUser);request.getHeaders().add(CUSTOM-REQUEST-HEADER,json);}returnexecution.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插件重置——替换默认并发策略packagecom.verycloud.platform.utils.hystrix;ConfigurationpublicclassThreadLocalConfiguration{Autowired(requiredfalse)privateHystrixConcurrencyStrategyexistingConcurrencyStrategy;PostConstructpublicvoidinit(){// 备份Hystrix已有的所有插件HystrixEventNotifiereventNotifierHystrixPlugins.getInstance().getEventNotifier();HystrixMetricsPublishermetricsPublisherHystrixPlugins.getInstance().getMetricsPublisher();HystrixPropertiesStrategypropertiesStrategyHystrixPlugins.getInstance().getPropertiesStrategy();HystrixCommandExecutionHookcommandExecutionHookHystrixPlugins.getInstance().getCommandExecutionHook();HystrixPlugins.reset();// 注入自定义并发策略保留已有插件不变HystrixPlugins.getInstance().registerConcurrencyStrategy(newThreadLocalAwareStrategy(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 自定义并发策略——包装Callablepackagecom.verycloud.platform.utils.hystrix;publicclassThreadLocalAwareStrategyextendsHystrixConcurrencyStrategy{privateHystrixConcurrencyStrategyexistingConcurrencyStrategy;publicThreadLocalAwareStrategy(HystrixConcurrencyStrategyexistingConcurrencyStrategy){this.existingConcurrencyStrategyexistingConcurrencyStrategy;}OverridepublicTCallableTwrapCallable(CallableTcallable){returnexistingConcurrencyStrategy!null?existingConcurrencyStrategy.wrapCallable(newDelegatingUserContextCallableT(callable,CurrentLoginUserHolder.getCurrentLoginUser())):super.wrapCallable(newDelegatingUserContextCallableT(callable,CurrentLoginUserHolder.getCurrentLoginUser()));}}wrapCallable()在Hystrix把任务提交到线程池之前被调用。这里劫持了传入的Callable——把它包进DelegatingUserContextCallable里。在当前线程主线程尚未离开这个调用点之前把ThreadLocal里的用户对象快照下来传给真正执行的线程。5.3 代理Callable——恢复上下文packagecom.verycloud.platform.utils.hystrix;publicclassDelegatingUserContextCallableVimplementsCallableV{privateCallableVdelegate;privateCurrentLoginUseroriginalUserInfo;publicDelegatingUserContextCallable(CallableVdelegate,CurrentLoginUseruserInfo){this.delegatedelegate;this.originalUserInfouserInfo;}OverridepublicVcall()throwsException{// 在新线程中恢复用户上下文CurrentLoginUserHolder.setCurrentLoginUser(originalUserInfo);try{returndelegate.call();}finally{this.originalUserInfonull;}}}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 入站 └── 同② → 循环继续三条线各司其职线组件解决的问题入站WrapApiResultInterceptorHTTP Header → ThreadLocal出站MyRestTemplateInterceptorThreadLocal → HTTP Header线程池穿透ThreadLocalAwareStrategyDelegatingUserContextCallableThreadLocal不会被线程池吃掉七、结语这套方案的核心原则业务代码永远不感知用户信息的传递机制。任何一个Service、任何一个Mapper、任何一个工具类——只要调CurrentLoginUserHolder.getCurrentLoginUser()就能拿到当前用户。不需要从Controller层层传参不需要在Feign接口上声明RequestHeader不需要在Hystrix代码里手动传ThreadLocal。三条线覆盖了同步传递、异步穿透、服务间转发三个场景。Hystrix插件的替换是整条方案里最容易被忽略的一环——定时任务、异步处理如果用线程池隔离但没有这份策略跑在子线程里的代码拿不到用户信息审计日志就丢了关键字段。