微服务安全上下文的透明传递——ThreadLocal透传与HTTP头转发的完整链路

微服务安全上下文的透明传递------ThreadLocal透传与HTTP头转发的完整链路

网关鉴权之后,下游的每个微服务都需要知道"当前请求是谁"。如果不做处理,每个Controller都要从Header里解析一遍用户信息,每个RestTemplate调用都要把用户信息塞回Header。这套方案用 ThreadLocal + 拦截器 + Hystrix策略 三条线实现了安全上下文的零感知传递------业务代码一行不改,在任何地方都能拿到当前用户,跨服务调用自动携带。

文章目录


一、问题:用户信息在微服务调用链中怎么传递

复制代码
用户请求 → Gateway(鉴权) → Service A → RestTemplate → Service B → Service C

每层都可能需要知道"是谁在访问"------审计日志、数据权限、操作记录都需要当前用户ID。三个问题:

  1. 入站:Gateway已经把用户信息鉴权过了,下游服务不需要再鉴权,但怎么拿到"这个人是谁"
  2. 出站:Service A调Service B时,怎么把用户信息从当前请求传递到下一个请求
  3. 线程池: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插件的替换是整条方案里最容易被忽略的一环------定时任务、异步处理如果用线程池隔离但没有这份策略,跑在子线程里的代码拿不到用户信息,审计日志就丢了关键字段。

相关推荐
QXWZ_IA30 分钟前
低空经济下电力无人机巡检定位的能力演进与应用趋势
人工智能·安全
亚远景aspice2 小时前
亚远景 - 无方向盘 L4 车辆批量认证,ASPICE 过程证据成制造商安全举证底座
安全·自动驾驶·aspice·研发管理·亚远景科技·过程能力评估·aspice评估
可涵不会debug2 小时前
PHP2Shell 爆发一周后:CVE-2026-109618/109619 自查、修复与防护实录
安全·php·云安全·waf·phpshell
福大大架构师每日一题3 小时前
excelize v2.11.1发布:Go 1.26.0起步,16项安全修复与嵌入对象能力全面升级
开发语言·安全·golang
yunlaodacom3 小时前
做异地灾备,华为云国际站 OBS 跨地域复制该怎么配置?
安全·华为云·云计算
江湖中的阿龙3 小时前
Oracle 紧急安全警报:Java 9.1 分漏洞 CVE-2026-91403,撤销对 eMudhra 风险证书的信任
java·安全·oracle
小贾要学习4 小时前
【C++后端开发项目】Reactor 模型 / C++ 仿写 muduo HTTP 服务器:让底层网络库真正跑起网页
linux·服务器·c++·http·网络编程·muduo·reactor模型
是真的小外套4 小时前
网络安全实战第21期:一次完整的注入风险验证与记录
安全·web安全
山东科恩光电5 小时前
安全地毯压敏传感防护在工业机器人与流水线区域的耐腐蚀材质及快速停机应用
安全
奈落2410 小时前
AI辅助的另一面:从2026年国际AI安全报告的“故障”清单到国内专家的可靠性短板,自主行动的Agent为什么更难及时干预
人工智能·安全·agent·本地部署·ai 办公