Skip to content

全链路上下文透传 Gateway -> 下游 -> Feign

8.1 什么是上下文透传?

上下文透传指的是:

text
一次请求进入系统后,某些上下文信息要沿着整个调用链传递下去。

常见上下文:

text
X-Request-Id:请求唯一 ID
X-Trace-Id:链路追踪 ID
X-User-Id:当前用户 ID
X-Tenant-Id:租户 ID
Authorization:登录令牌
Accept-Language:语言

调用链:

text
客户端

Gateway

order-service

Feign

stock-service

这些 Header 应该被一路传下去。


8.2 为什么需要上下文透传?

用于:

text
链路追踪
日志排查
用户身份识别
租户隔离
权限控制
灰度发布
审计记录

例如排查日志时:

text
X-Request-Id = abc-123

可以在网关、订单服务、库存服务日志中搜索同一个 ID。


8.3 透传设计原则

建议只透传白名单 Header:

text
X-Request-Id
X-Trace-Id
X-User-Id
X-Tenant-Id
Authorization
Accept-Language

不要无脑透传所有 Header。

原因:

text
可能引入安全风险
可能暴露内部 Header
可能导致 Header 过大
可能把伪造的用户信息传入后端

8.4 Gateway 生成 RequestId

如果客户端没有传 X-Request-Id,网关生成一个。

java
package com.demo.gateway.filter;

import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.UUID;

@Component
public class RequestIdGlobalFilter implements GlobalFilter, Ordered {

    public static final String REQUEST_ID = "X-Request-Id";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange,
                             GatewayFilterChain chain) {

        String requestId = exchange.getRequest().getHeaders().getFirst(REQUEST_ID);

        if (requestId == null || requestId.isBlank()) {
            requestId = UUID.randomUUID().toString();
        }

        ServerHttpRequest newRequest = exchange.getRequest()
                .mutate()
                .header(REQUEST_ID, requestId)
                .build();

        ServerWebExchange newExchange = exchange.mutate()
                .request(newRequest)
                .build();

        return chain.filter(newExchange);
    }

    @Override
    public int getOrder() {
        return -200;
    }
}

8.5 Gateway 模拟鉴权并透传用户信息

实际项目中,Gateway 会解析 token。

这里为了教学,先简单模拟。

java
package com.demo.gateway.filter;

import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class AuthMockGlobalFilter implements GlobalFilter, Ordered {

    public static final String USER_ID = "X-User-Id";
    public static final String TENANT_ID = "X-Tenant-Id";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange,
                             GatewayFilterChain chain) {

        ServerHttpRequest newRequest = exchange.getRequest()
                .mutate()
                .header(USER_ID, "10001")
                .header(TENANT_ID, "tenant-a")
                .build();

        return chain.filter(exchange.mutate().request(newRequest).build());
    }

    @Override
    public int getOrder() {
        return -150;
    }
}

8.6 下游服务读取 Header

order-service 中:

java
package com.demo.order.controller;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/context")
public class ContextController {

    @GetMapping("/show")
    public String show(@RequestHeader(value = "X-Request-Id", required = false) String requestId,
                       @RequestHeader(value = "X-User-Id", required = false) String userId,
                       @RequestHeader(value = "X-Tenant-Id", required = false) String tenantId) {

        return "order-service received: "
                + "requestId=" + requestId
                + ", userId=" + userId
                + ", tenantId=" + tenantId;
    }
}

访问:

text
http://localhost:9000/api/order/context/show

期望结果:

text
order-service received: requestId=xxx, userId=10001, tenantId=tenant-a

8.7 Servlet Filter 解析 Header 存入 ThreadLocal

如果希望在整个请求处理过程中都能方便地获取上下文,可以把 Header 存入 ThreadLocal。

ContextHolder:

java
package com.demo.common.context;

public class UserContext {

    private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();
    private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
    private static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();

    public static void setRequestId(String requestId) {
        REQUEST_ID.set(requestId);
    }

    public static String getRequestId() {
        return REQUEST_ID.get();
    }

    public static void setUserId(String userId) {
        USER_ID.set(userId);
    }

    public static String getUserId() {
        return USER_ID.get();
    }

    public static void setTenantId(String tenantId) {
        TENANT_ID.set(tenantId);
    }

    public static String getTenantId() {
        return TENANT_ID.get();
    }

    public static void clear() {
        REQUEST_ID.remove();
        USER_ID.remove();
        TENANT_ID.remove();
    }
}

Servlet Filter:

java
package com.demo.order.filter;

import com.demo.common.context.UserContext;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class UserContextFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;

        String requestId = httpRequest.getHeader("X-Request-Id");
        String userId = httpRequest.getHeader("X-User-Id");
        String tenantId = httpRequest.getHeader("X-Tenant-Id");

        try {
            UserContext.setRequestId(requestId);
            UserContext.setUserId(userId);
            UserContext.setTenantId(tenantId);

            chain.doFilter(request, response);
        } finally {
            UserContext.clear();
        }
    }
}

8.8 Feign RequestInterceptor 传递 Header

如果 order-service 要通过 Feign 调用 stock-service,需要把 Header 继续传下去。

java
package com.demo.order.feign;

import com.demo.common.context.UserContext;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;

@Component
public class UserContextRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {

        String requestId = UserContext.getRequestId();
        String userId = UserContext.getUserId();
        String tenantId = UserContext.getTenantId();

        if (requestId != null) {
            template.header("X-Request-Id", requestId);
        }

        if (userId != null) {
            template.header("X-User-Id", userId);
        }

        if (tenantId != null) {
            template.header("X-Tenant-Id", tenantId);
        }
    }
}

8.9 验证全链路透传

访问:

text
http://localhost:9000/api/order/context/feign

order-service Controller:

java
package com.demo.order.controller;

import com.demo.order.client.StockClient;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/context")
public class FeignContextController {

    private final StockClient stockClient;

    public FeignContextController(StockClient stockClient) {
        this.stockClient = stockClient;
    }

    @GetMapping("/feign")
    public String feignContext() {
        return stockClient.context();
    }
}

Feign:

java
package com.demo.order.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(name = "stock-service")
public interface StockClient {

    @GetMapping("/stock/context")
    String context();
}

访问:

text
http://localhost:9000/api/order/order/context/feign

期望结果:

text
stock-service received: requestId=xxx, userId=10001, tenantId=tenant-a

8.10 上下文透传注意事项

注意 1:Gateway 是 WebFlux,不能使用 Servlet API

Gateway 中不能使用:

java
HttpServletRequest
HttpServletResponse
Filter

应该使用:

java
ServerWebExchange
ServerHttpRequest
GlobalFilter

注意 2:下游 Spring MVC 服务可以使用 Servlet Filter

普通 Web 服务中可以使用:

java
jakarta.servlet.Filter
HttpServletRequest

注意 3:ThreadLocal 在线程切换时会丢失

如果业务中使用:

text
异步线程
线程池
CompletableFuture
@Async
Reactor

ThreadLocal 可能无法自动传递。

解决思路:

text
显式传参
使用 TransmittableThreadLocal
在线程池包装任务
使用链路追踪组件

注意 4:不要盲目信任前端传来的用户 Header

例如:

text
X-User-Id
X-Tenant-Id

不应该直接相信客户端传入。

推荐:

text
客户端只传 Authorization
Gateway 解析 token
Gateway 生成可信的 X-User-Id / X-Tenant-Id
下游服务只信任来自 Gateway 的内部 Header

Released under the MIT License.