微服务安全上下文的透明传递——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 替换默认的。其余的 eventNotifiermetricsPublisherpropertiesStrategycommandExecutionHook 保持原样。

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插件的替换是整条方案里最容易被忽略的一环------定时任务、异步处理如果用线程池隔离但没有这份策略,跑在子线程里的代码拿不到用户信息,审计日志就丢了关键字段。

相关推荐
peterfei1 小时前
ai-agent-scan v1.0.0:基于 MCP 协议的开源 SAST 安全扫描器
安全·ai编程
触底反弹1 小时前
从 Bun 到 DeepSeek:用 TypeScript 构建你的第一个 AI Agent
人工智能·http·typescript
汽车仪器仪表相关领域2 小时前
南华 NHASM-1 型稳态工况法汽车排气检测系统|国标合规汽油车工况检测专用设备
功能测试·安全·单元测试·汽车·压力测试·可用性测试
SilentSamsara2 小时前
DuckDB + Python:嵌入式 OLAP 数据库的轻量分析实战
开发语言·数据库·python·微服务
飞函安全2 小时前
企业网盘和IM打通后,文件审批、版本更新和权限回收能自动化到什么程度
安全·私有化im
Jay-r2 小时前
智能合约开发中13种最常见漏洞及修复(精华版)
安全·web安全·区块链·智能合约·solidity
JAVA面经实录9172 小时前
Spring Cloud Alibaba 微服务企业实战完整文档(架构+规范+调优+故障+源码)
java·运维·spring cloud·微服务
Warren2Lynch2 小时前
破局“伪敏捷”:UML诊断视角下的微服务转型与架构重构——以EcoStream为例
微服务·架构·uml
开开心心就好2 小时前
清理重复文件释放C盘空间的工具
安全·智能手机·pdf·gitlab·音视频·intellij idea·1024程序员节