网关、服务 userId 透传

网关、服务 userId 透传

微服务里一个请求要经过网关 → A 服务 → A 调 B 服务。userId 怎么从头传到尾?为什么 Feign 调用时 Header 是空的?怎么补上?


先搞清楚角色关系

任何一个微服务请求都经过这几步:
#mermaid-svg-53jPA8cVj5pY0sIB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-53jPA8cVj5pY0sIB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-53jPA8cVj5pY0sIB .error-icon{fill:#552222;}#mermaid-svg-53jPA8cVj5pY0sIB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-53jPA8cVj5pY0sIB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-53jPA8cVj5pY0sIB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-53jPA8cVj5pY0sIB .marker.cross{stroke:#333333;}#mermaid-svg-53jPA8cVj5pY0sIB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-53jPA8cVj5pY0sIB p{margin:0;}#mermaid-svg-53jPA8cVj5pY0sIB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-53jPA8cVj5pY0sIB .cluster-label text{fill:#333;}#mermaid-svg-53jPA8cVj5pY0sIB .cluster-label span{color:#333;}#mermaid-svg-53jPA8cVj5pY0sIB .cluster-label span p{background-color:transparent;}#mermaid-svg-53jPA8cVj5pY0sIB .label text,#mermaid-svg-53jPA8cVj5pY0sIB span{fill:#333;color:#333;}#mermaid-svg-53jPA8cVj5pY0sIB .node rect,#mermaid-svg-53jPA8cVj5pY0sIB .node circle,#mermaid-svg-53jPA8cVj5pY0sIB .node ellipse,#mermaid-svg-53jPA8cVj5pY0sIB .node polygon,#mermaid-svg-53jPA8cVj5pY0sIB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-53jPA8cVj5pY0sIB .rough-node .label text,#mermaid-svg-53jPA8cVj5pY0sIB .node .label text,#mermaid-svg-53jPA8cVj5pY0sIB .image-shape .label,#mermaid-svg-53jPA8cVj5pY0sIB .icon-shape .label{text-anchor:middle;}#mermaid-svg-53jPA8cVj5pY0sIB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-53jPA8cVj5pY0sIB .rough-node .label,#mermaid-svg-53jPA8cVj5pY0sIB .node .label,#mermaid-svg-53jPA8cVj5pY0sIB .image-shape .label,#mermaid-svg-53jPA8cVj5pY0sIB .icon-shape .label{text-align:center;}#mermaid-svg-53jPA8cVj5pY0sIB .node.clickable{cursor:pointer;}#mermaid-svg-53jPA8cVj5pY0sIB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-53jPA8cVj5pY0sIB .arrowheadPath{fill:#333333;}#mermaid-svg-53jPA8cVj5pY0sIB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-53jPA8cVj5pY0sIB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-53jPA8cVj5pY0sIB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-53jPA8cVj5pY0sIB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-53jPA8cVj5pY0sIB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-53jPA8cVj5pY0sIB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-53jPA8cVj5pY0sIB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-53jPA8cVj5pY0sIB .cluster text{fill:#333;}#mermaid-svg-53jPA8cVj5pY0sIB .cluster span{color:#333;}#mermaid-svg-53jPA8cVj5pY0sIB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-53jPA8cVj5pY0sIB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-53jPA8cVj5pY0sIB rect.text{fill:none;stroke-width:0;}#mermaid-svg-53jPA8cVj5pY0sIB .icon-shape,#mermaid-svg-53jPA8cVj5pY0sIB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-53jPA8cVj5pY0sIB .icon-shape p,#mermaid-svg-53jPA8cVj5pY0sIB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-53jPA8cVj5pY0sIB .icon-shape .label rect,#mermaid-svg-53jPA8cVj5pY0sIB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-53jPA8cVj5pY0sIB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-53jPA8cVj5pY0sIB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-53jPA8cVj5pY0sIB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求 A

Header: userId = 10001
Feign 调用
新请求 B

Header 空的
客户端
Gateway(网关)

鉴权通过后,把 userId 塞进 Header

转发给下游
服务 A

Filter 从 Header 读 userId → 写入 TTL

内部 Controller → Service → DAO 随时可拿
⚠️ Feign 发起全新 HTTP 请求

这是新的 TCP 连接,Header 是空的!

网关只给 A 加了 Header

这个新请求没人给它加
服务 B

Filter 从 Header 读 userId

空的 → TTL 也是空的 → 链路断了

Feign 调用是通过 HTTP 协议向另一个服务发起新的请求,这个请求跟网关转发过来的那个请求没有关系------它是一条新的 TCP 连接,Header 是空的。而 GatewayFilter 只负责给经由它转发的请求加 Header。

三个组件各管一段,组成一条接力链:

组件 位置 干了什么 没有它会怎样
Gateway Filter 网关 鉴权通过后,把 userId 塞进 Header,转发给下游 请求进到服务 A 时 Header 里没有 userId,谁都不知道是谁
Context Filter 每个服务内部 请求进来时从 Header 读 userId → 存入 TTL Controller/Service/DAO 到处都得传 userId 参数
Feign Interceptor 服务 A 调 B 时 从 TTL 读 userId → 塞进 Feign 新请求的 Header 服务 B 的 Header 是空的,链路中断

需要引入的依赖

三个组件涉及四个依赖,各管各的:

依赖 用途 哪个组件在用
spring-cloud-starter-gateway Gateway Filter 网关模块
spring-boot-starter-web OncePerRequestFilterFilterRegistrationBean Context Filter(每个服务)
feign-core RequestInterceptor 接口 Feign Interceptor(每个服务)
transmittable-thread-local TTL,跨线程池传递 userId Context(每个服务)

网关模块的 pom.xml:

xml 复制代码
<!-- Gateway 依赖(WebFlux 环境,不要同时引入 spring-boot-starter-web) -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

每个业务服务的 pom.xml:

xml 复制代码
<!-- Servlet 环境,提供 Filter 接口 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Feign 核心包,提供 RequestInterceptor 接口 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-core</artifactId>
</dependency>

<!-- 阿里 TTL:替代普通 ThreadLocal,线程池里也能传递 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.5</version>
</dependency>

注意:spring-cloud-starter-gateway 基于 WebFlux,和 spring-boot-starter-web(基于 Servlet)不能放在同一个模块里,否则启动报错。网关模块只引 Gateway,业务模块只引 Web。

实际项目里可以把 Context Filter + TTL 容器 + Feign 拦截器抽成一个公共 starter(只要是请求到 A 服务这里我就用 Context Filter 取 Header,存入 TTL,这样 A 服务内部使用没问题,调用 B 服务时把 id 存入新的请求的 Heaer,下一个服务继续读取,存入 TTL...),业务模块只需要引这一个:

xml 复制代码
<!-- 你自己的公共 starter -->
<dependency>
    <groupId>com.your-company</groupId>
    <artifactId>biz-context-starter</artifactId>
</dependency>

<!-- 另外还要引入 OpenFeign 的 starter 和你要调用的服务的 Feign 接口包 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>com.your-company</groupId>
    <artifactId>user-service-api</artifactId>
</dependency>

其中 biz-context-starter 内部把 feign-coretransmittable-thread-local 打包进去了,业务模块不用再单独声明。


第一段:Gateway Filter ------ 让所有下游服务知道是谁

网关是整个系统的唯一入口。不管前端请求最终被路由到哪个服务,都要先过网关。所以在网关里把 userId 放进 Header,是最合理的位置。

java 复制代码
@Component
@Order(-99)
public class AddUserId2HeaderFilter implements GlobalFilter {

    private static final String HEADER_USER_ID = "userId";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 从鉴权框架获取当前登录的 userId(这里以 Sa-Token 为例)
        Long userId = getLoginUserId();

        if (userId == null) {
            return chain.filter(exchange);
        }

        // 把 userId 塞进 Header,所有下游服务都能读到
        ServerWebExchange newExchange = exchange.mutate()
                .request(builder -> builder.headers(headers -> {
                    headers.remove(HEADER_USER_ID);       // 防止前端伪造
                    headers.add(HEADER_USER_ID, String.valueOf(userId));
                }))
                .build();

        return chain.filter(newExchange);
    }
}

三个注意点:

  • headers.remove(HEADER_USER_ID)------前端可能在请求里自己塞了一个假的 userId,先删再写,防止伪造
  • exchange.mutate()------Gateway 的请求对象不可变,只能创建新的 Exchange 往下传
  • 不管鉴权框架用的是 Sa-Token、Spring Security 还是自研的,思路都一样:鉴权通过后拿到 userId → 塞 Header

第二段:Context Filter ------ 让服务内部随时随地拿到 userId

请求进了某个服务,Header 里已经有网关放进去的 userId 了。但如果 Controller 每次从 HttpServletRequest 取、Service 再从参数传、DAO 再从参数传------太蠢,而且跟 HTTP 耦合。

在 Filter 里把 Header 的值取出来,存进 TTL。后续整个请求链路(Controller → Service → DAO)直接从 TTL 拿,就像拿一个全局变量。

java 复制代码
// Filter:请求进来读 Header 写 TTL,请求结束清 TTL
@Slf4j
public class UserContextFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {
        String userId = request.getHeader("userId");

        if (StringUtils.isBlank(userId)) {
            chain.doFilter(request, response);
            return;
        }

        // 写入 TTL
        UserContext.setUserId(userId);
        try {
            chain.doFilter(request, response);
        } finally {
            UserContext.remove();  // 请求结束必须清,否则内存泄漏 + 数据串了
        }
    }
}
java 复制代码
// TTL 容器------业务代码唯一需要接触的类
public class UserContext {

    // 为什么是 TTL 而不是普通 ThreadLocal?见后文
    private static final ThreadLocal<Map<String, Object>> CONTEXT
            = TransmittableThreadLocal.withInitial(HashMap::new);

    public static void setUserId(Object value) {
        CONTEXT.get().put("userId", value);
    }

    public static Long getUserId() {
        Object value = CONTEXT.get().get("userId");
        if (value == null) { return null; }
        return Long.valueOf(value.toString());
    }

    public static void remove() {
        CONTEXT.remove();
    }
}

这个 Filter 用 Spring Boot 自动配置注册,打成 starter,业务模块引入依赖即可:

java 复制代码
@AutoConfiguration
public class UserContextAutoConfiguration {

    @Bean
    public FilterRegistrationBean<UserContextFilter> userContextFilter() {
        return new FilterRegistrationBean<>(new UserContextFilter());
    }
}

到此为止,同一个服务内部 的 userId 传递已经解决了。业务代码任何地方直接 UserContext.getUserId()


第三段:Feign Interceptor ------ 跨服务调用时把 userId 塞进新请求

一个服务的 Controller → Service → DAO 链路里,如果需要调另一个服务,通常是发一个 Feign 请求。

Feign 发的是一个全新的 HTTP 请求。新请求的 Header 是空的。 下游服务的 Context Filter 从 Header 读到 null → TTL 也是空的。
服务 B Feign(新 HTTP 请求) 服务 A 服务 B Feign(新 HTTP 请求) 服务 A #mermaid-svg-8OGA6uz88Q4nNBmk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8OGA6uz88Q4nNBmk .error-icon{fill:#552222;}#mermaid-svg-8OGA6uz88Q4nNBmk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8OGA6uz88Q4nNBmk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8OGA6uz88Q4nNBmk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8OGA6uz88Q4nNBmk .marker.cross{stroke:#333333;}#mermaid-svg-8OGA6uz88Q4nNBmk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8OGA6uz88Q4nNBmk p{margin:0;}#mermaid-svg-8OGA6uz88Q4nNBmk .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8OGA6uz88Q4nNBmk text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8OGA6uz88Q4nNBmk .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-8OGA6uz88Q4nNBmk .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-8OGA6uz88Q4nNBmk #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-8OGA6uz88Q4nNBmk .sequenceNumber{fill:white;}#mermaid-svg-8OGA6uz88Q4nNBmk #sequencenumber{fill:#333;}#mermaid-svg-8OGA6uz88Q4nNBmk #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-8OGA6uz88Q4nNBmk .messageText{fill:#333;stroke:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8OGA6uz88Q4nNBmk .labelText,#mermaid-svg-8OGA6uz88Q4nNBmk .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .loopText,#mermaid-svg-8OGA6uz88Q4nNBmk .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8OGA6uz88Q4nNBmk .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-8OGA6uz88Q4nNBmk .noteText,#mermaid-svg-8OGA6uz88Q4nNBmk .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-8OGA6uz88Q4nNBmk .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8OGA6uz88Q4nNBmk .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8OGA6uz88Q4nNBmk .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8OGA6uz88Q4nNBmk .actorPopupMenu{position:absolute;}#mermaid-svg-8OGA6uz88Q4nNBmk .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-8OGA6uz88Q4nNBmk .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8OGA6uz88Q4nNBmk .actor-man circle,#mermaid-svg-8OGA6uz88Q4nNBmk line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-8OGA6uz88Q4nNBmk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TTL 里有 userId=10001 ✅ 这是个新请求Header 是空的TTL 传不过去 Filter 读 Header → nullTTL 也是空的💥 不知道是谁在请求 userClient.getById(10001)GET /user/10001Header: (空)

所以需要一个 Feign 拦截器:在 Feign 发出请求之前,自己从当前线程的 TTL 里取出 userId,塞进去。

java 复制代码
@Slf4j
public class UserContextFeignInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        Long userId = UserContext.getUserId();

        if (userId != null) {
            // 把这个 userId 塞进 Feign 请求的 Header
            requestTemplate.header("userId", String.valueOf(userId));
        }
    }
}

自动配置------全局生效:

java 复制代码
@AutoConfiguration
public class FeignContextAutoConfiguration {

    @Bean
    public UserContextFeignInterceptor userContextFeignInterceptor() {
        return new UserContextFeignInterceptor();
    }
}

加上之后:
服务 B Feign(新 HTTP 请求) 服务 A 服务 B Feign(新 HTTP 请求) 服务 A #mermaid-svg-bG3Jgzt073nyaAMU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bG3Jgzt073nyaAMU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bG3Jgzt073nyaAMU .error-icon{fill:#552222;}#mermaid-svg-bG3Jgzt073nyaAMU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bG3Jgzt073nyaAMU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bG3Jgzt073nyaAMU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bG3Jgzt073nyaAMU .marker.cross{stroke:#333333;}#mermaid-svg-bG3Jgzt073nyaAMU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bG3Jgzt073nyaAMU p{margin:0;}#mermaid-svg-bG3Jgzt073nyaAMU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bG3Jgzt073nyaAMU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-bG3Jgzt073nyaAMU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bG3Jgzt073nyaAMU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-bG3Jgzt073nyaAMU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-bG3Jgzt073nyaAMU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-bG3Jgzt073nyaAMU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-bG3Jgzt073nyaAMU .sequenceNumber{fill:white;}#mermaid-svg-bG3Jgzt073nyaAMU #sequencenumber{fill:#333;}#mermaid-svg-bG3Jgzt073nyaAMU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-bG3Jgzt073nyaAMU .messageText{fill:#333;stroke:none;}#mermaid-svg-bG3Jgzt073nyaAMU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bG3Jgzt073nyaAMU .labelText,#mermaid-svg-bG3Jgzt073nyaAMU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-bG3Jgzt073nyaAMU .loopText,#mermaid-svg-bG3Jgzt073nyaAMU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-bG3Jgzt073nyaAMU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bG3Jgzt073nyaAMU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-bG3Jgzt073nyaAMU .noteText,#mermaid-svg-bG3Jgzt073nyaAMU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-bG3Jgzt073nyaAMU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bG3Jgzt073nyaAMU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bG3Jgzt073nyaAMU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bG3Jgzt073nyaAMU .actorPopupMenu{position:absolute;}#mermaid-svg-bG3Jgzt073nyaAMU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-bG3Jgzt073nyaAMU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bG3Jgzt073nyaAMU .actor-man circle,#mermaid-svg-bG3Jgzt073nyaAMU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-bG3Jgzt073nyaAMU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TTL 里有 userId=10001 ✅ 拦截器从 TTL 读完写进 Header: userId=10001 Filter 读 Header → 写 TTL ✅后续链路都能拿到 userId userClient.getById(10001)GET /user/10001Header: userId=10001


完整链路串起来

服务 B 服务 A Gateway 客户端 服务 B 服务 A Gateway 客户端 #mermaid-svg-rwCjIvA1SEEHs0bh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rwCjIvA1SEEHs0bh .error-icon{fill:#552222;}#mermaid-svg-rwCjIvA1SEEHs0bh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rwCjIvA1SEEHs0bh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rwCjIvA1SEEHs0bh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rwCjIvA1SEEHs0bh .marker.cross{stroke:#333333;}#mermaid-svg-rwCjIvA1SEEHs0bh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rwCjIvA1SEEHs0bh p{margin:0;}#mermaid-svg-rwCjIvA1SEEHs0bh .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rwCjIvA1SEEHs0bh text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-rwCjIvA1SEEHs0bh .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-rwCjIvA1SEEHs0bh .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-rwCjIvA1SEEHs0bh #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-rwCjIvA1SEEHs0bh .sequenceNumber{fill:white;}#mermaid-svg-rwCjIvA1SEEHs0bh #sequencenumber{fill:#333;}#mermaid-svg-rwCjIvA1SEEHs0bh #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-rwCjIvA1SEEHs0bh .messageText{fill:#333;stroke:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rwCjIvA1SEEHs0bh .labelText,#mermaid-svg-rwCjIvA1SEEHs0bh .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .loopText,#mermaid-svg-rwCjIvA1SEEHs0bh .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-rwCjIvA1SEEHs0bh .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-rwCjIvA1SEEHs0bh .noteText,#mermaid-svg-rwCjIvA1SEEHs0bh .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-rwCjIvA1SEEHs0bh .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rwCjIvA1SEEHs0bh .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rwCjIvA1SEEHs0bh .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rwCjIvA1SEEHs0bh .actorPopupMenu{position:absolute;}#mermaid-svg-rwCjIvA1SEEHs0bh .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-rwCjIvA1SEEHs0bh .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rwCjIvA1SEEHs0bh .actor-man circle,#mermaid-svg-rwCjIvA1SEEHs0bh line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-rwCjIvA1SEEHs0bh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① Gateway Filter鉴权 → userId=10001塞进 Header ② Context FilterHeader 读 userId → 写 TTL ③ 需要调服务 B ② Context FilterHeader 读 userId → 写 TTL 请求 + Authorization: Bearer xxx转发(Header: userId=10001)Controller → ServiceUserContext.getUserId() → 10001 ✅FeignRequestInterceptorTTL 读 userId → 写新请求 HeaderPOST /user/findByPhoneHeader: userId=10001Controller → ServiceUserContext.getUserId() → 10001 ✅响应响应响应

三段接力,各解决一个问题:
#mermaid-svg-pgVECdyzr77I50uE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pgVECdyzr77I50uE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pgVECdyzr77I50uE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pgVECdyzr77I50uE .error-icon{fill:#552222;}#mermaid-svg-pgVECdyzr77I50uE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pgVECdyzr77I50uE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pgVECdyzr77I50uE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pgVECdyzr77I50uE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pgVECdyzr77I50uE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pgVECdyzr77I50uE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pgVECdyzr77I50uE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pgVECdyzr77I50uE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pgVECdyzr77I50uE .marker.cross{stroke:#333333;}#mermaid-svg-pgVECdyzr77I50uE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pgVECdyzr77I50uE p{margin:0;}#mermaid-svg-pgVECdyzr77I50uE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pgVECdyzr77I50uE .cluster-label text{fill:#333;}#mermaid-svg-pgVECdyzr77I50uE .cluster-label span{color:#333;}#mermaid-svg-pgVECdyzr77I50uE .cluster-label span p{background-color:transparent;}#mermaid-svg-pgVECdyzr77I50uE .label text,#mermaid-svg-pgVECdyzr77I50uE span{fill:#333;color:#333;}#mermaid-svg-pgVECdyzr77I50uE .node rect,#mermaid-svg-pgVECdyzr77I50uE .node circle,#mermaid-svg-pgVECdyzr77I50uE .node ellipse,#mermaid-svg-pgVECdyzr77I50uE .node polygon,#mermaid-svg-pgVECdyzr77I50uE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pgVECdyzr77I50uE .rough-node .label text,#mermaid-svg-pgVECdyzr77I50uE .node .label text,#mermaid-svg-pgVECdyzr77I50uE .image-shape .label,#mermaid-svg-pgVECdyzr77I50uE .icon-shape .label{text-anchor:middle;}#mermaid-svg-pgVECdyzr77I50uE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pgVECdyzr77I50uE .rough-node .label,#mermaid-svg-pgVECdyzr77I50uE .node .label,#mermaid-svg-pgVECdyzr77I50uE .image-shape .label,#mermaid-svg-pgVECdyzr77I50uE .icon-shape .label{text-align:center;}#mermaid-svg-pgVECdyzr77I50uE .node.clickable{cursor:pointer;}#mermaid-svg-pgVECdyzr77I50uE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pgVECdyzr77I50uE .arrowheadPath{fill:#333333;}#mermaid-svg-pgVECdyzr77I50uE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pgVECdyzr77I50uE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pgVECdyzr77I50uE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pgVECdyzr77I50uE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pgVECdyzr77I50uE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pgVECdyzr77I50uE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pgVECdyzr77I50uE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pgVECdyzr77I50uE .cluster text{fill:#333;}#mermaid-svg-pgVECdyzr77I50uE .cluster span{color:#333;}#mermaid-svg-pgVECdyzr77I50uE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pgVECdyzr77I50uE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pgVECdyzr77I50uE rect.text{fill:none;stroke-width:0;}#mermaid-svg-pgVECdyzr77I50uE .icon-shape,#mermaid-svg-pgVECdyzr77I50uE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pgVECdyzr77I50uE .icon-shape p,#mermaid-svg-pgVECdyzr77I50uE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pgVECdyzr77I50uE .icon-shape .label rect,#mermaid-svg-pgVECdyzr77I50uE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pgVECdyzr77I50uE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pgVECdyzr77I50uE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pgVECdyzr77I50uE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ③ Feign Interceptor:掏口袋盖章
服务 A 要调服务 B
Feign 发新请求,Header 是空的
从 TTL 掏出 userId
盖在新请求的 Header 上
下游服务又能读到
② Context Filter:进口袋
请求进入服务
从 Header 取出 userId
放进 TTL(内存口袋)
Controller/Service/DAO

随时能拿
① Gateway Filter:盖章
鉴权通过
Header 盖章:userId=10001
下游服务都能读到

口袋和章的比喻:

复制代码
Gateway Filter:
  所有人从公网进来,网关验明正身,在 Header 上盖个章"这是 10001"

Context Filter:
  服务收到请求,从 Header 取下这个章,放进 TTL(服务内部的"口袋")
  整个请求链路谁要谁从口袋拿,不用每次都从 Header 掏

Feign Interceptor:
  服务调另一个服务,发的是新请求,Header 是白纸
  从口袋掏出章,盖在新请求的 Header 上
  → 下游服务又能从 Header 取到 → 进口袋 → 循环继续

为什么用 TTL 而不是普通 ThreadLocal

服务内部如果用了线程池做异步任务,普通 ThreadLocal 在线程池里会丢值:

java 复制代码
// 主线程 set 了
UserContext.setUserId(10001L);

// 线程池线程没被 set 过 → null
executor.submit(() -> {
    Long id = UserContext.getUserId();  // null ❌
});

TransmittableThreadLocal(阿里开源)在每次提交任务时自动把值从父线程拷贝到线程池线程:

java 复制代码
// 方式一:包装每个任务
executor.submit(TtlRunnable.get(() -> {
    Long id = UserContext.getUserId();  // 10001 ✅
}));

// 方式二:包装线程池,一劳永逸
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(executor);

如果项目里没用线程池,普通 ThreadLocal 也够。但微服务里很难保证永远不用------随便接个异步日志、消息队列、CompletableFuture,线程池就出现了。所以直接用 TTL,省得后面排查半天。


附:工程结构建议

把三个组件------Filter、TTL 容器、Feign 拦截器------抽成一个公共 starter。所有业务模块引入即可,不用在每个服务里重复写。

复制代码
your-company-starters/
└── biz-context-starter/          ← 公共 starter
    ├── config/
    │   ├── UserContextAutoConfiguration.java      → 注册 Context Filter
    │   └── FeignContextAutoConfiguration.java     → 注册 Feign 拦截器
    ├── filter/
    │   └── UserContextFilter.java                  → Header → TTL
    ├── context/
    │   └── UserContext.java                        → TTL 容器
    └── interceptor/
        └── UserContextFeignInterceptor.java         → TTL → Feign Header

业务模块只需要:

xml 复制代码
<dependency>
    <groupId>com.your-company</groupId>
    <artifactId>biz-context-starter</artifactId>
</dependency>
java 复制代码
@SpringBootApplication
@EnableFeignClients(basePackages = "com.your-company")
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

启动类加 @EnableFeignClients 扫描所有 Feign 接口所在的包。

相关推荐
bksczm1 小时前
Linux之信号量(POSIX标准)
java·开发语言
Devin~Y1 小时前
电商场景下的Java面试实战:从Spring Boot微服务到Kafka、Redis与AI RAG
java·spring boot·redis·elasticsearch·spring cloud·微服务·kafka
吠品1 小时前
PHP里取月份最后一天的几种方式
java·开发语言·eureka
gugucoding1 小时前
2. 【Java】搭建Java开发环境
java·开发语言
SizeTheMoment2 小时前
Spring Cloud 微服务认证体系深度解析:从架构设计到安全实践
安全·spring cloud·微服务
wang09072 小时前
自己动手写一个spring之MVC_3
java·spring·mvc
闲猫2 小时前
Spring AI / Models / Chat Models / Ollama
java·人工智能·spring
FREEDOM_X2 小时前
Linux 多线程编程——总结
java·linux·运维·ubuntu
tmlx3I0813 小时前
Tomcat的架构设计和启动过程详解
java·tomcat