全链路上下文透传 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-a8.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/feignorder-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-a8.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
ReactorThreadLocal 可能无法自动传递。
解决思路:
text
显式传参
使用 TransmittableThreadLocal
在线程池包装任务
使用链路追踪组件注意 4:不要盲目信任前端传来的用户 Header
例如:
text
X-User-Id
X-Tenant-Id不应该直接相信客户端传入。
推荐:
text
客户端只传 Authorization
Gateway 解析 token
Gateway 生成可信的 X-User-Id / X-Tenant-Id
下游服务只信任来自 Gateway 的内部 Header