微服务之间通过 Feign 调用时,如何实现请求头(如 Token、Session)的透传?
在微服务架构中,由于 Feign 客户端在发起远程调用时,实际上是构建了一个全新的 HTTP 请求,因此默认情况下,原始请求中的 Header(如 Token、Session、Cookie 等)会丢失。
要实现 Header 的透传,最常用且优雅的方式是使用 Feign 的 RequestInterceptor(请求拦截器)。此外,处理异步调用时的上下文丢失问题也是关键。
以下是完整的实现方案及避坑指南:
方案一:使用全局拦截器(最常用,推荐)
通过实现 feign.RequestInterceptor 接口,在 Feign 发起请求前拦截并修改请求,将原始请求的 Header 复制到新请求中。
1. 编写拦截器代码
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 1. 获取当前请求的上下文
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
// 2. 遍历并获取原始请求头
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
// 3. 过滤掉 contentLength,否则可能导致 Feign 请求卡死或报错
if ("content-length".equalsIgnoreCase(name) || "host".equalsIgnoreCase(name)) {
continue;
}
String values = request.getHeader(name);
// 4. 将 Header 透传给 Feign 的 RequestTemplate
template.header(name, values);
}
}
}
}
}
注意:
- 上面的
@Configuration会让该拦截器对所有 FeignClient 生效。- 千万不要透传
Content-Length,因为 Feign 构建的新请求 body 大小可能和原请求不同,透传会导致目标服务解析报文阻塞或报错。
2. 局部生效(可选)
如果你只想让某个特定的 Feign 客户端透传 Header,可以去掉上面的 @Configuration 注解,然后在 @FeignClient 注解中显式指定:
@FeignClient(name = "user-service", configuration = FeignRequestInterceptor.class)
public interface UserFeignClient {
// ...
}
方案二:方法参数显式传递(适合个别接口)
如果你只有极个别接口需要传递 Token,或者不想使用拦截器,可以直接使用 @RequestHeader 注解在 Feign 接口定义处接收。
@FeignClient(name = "user-service")
public interface UserFeignClient {
@GetMapping("/api/user/info")
UserInfo getUserInfo(@RequestHeader("Authorization") String token);
}
调用处:
// 在 Service 层手动获取 Token 并传递
String token = request.getHeader("Authorization");
UserInfo info = userFeignClient.getUserInfo(token);
缺点:对业务代码有侵入性,每个方法都要加参数,不适合全局使用。
⚠️ 核心痛点:异步调用导致的 Header 丢失问题
问题描述:
在方案一中,我们使用了 RequestContextHolder.getRequestAttributes()。由于 RequestContextHolder 底层是基于 ThreadLocal 实现的,如果你在异步线程(如 @Async、CompletableFuture、Hystrix 的线程池隔离模式)中发起 Feign 调用,由于线程切换,将获取不到 ServletRequestAttributes,导致报空指针或透传失败。
解决异步丢失的方案:
解决思路 1:主线程向子线程手动传递
在提交异步任务之前,将上下文提取出来,传给子线程。
// 1. 主线程中获取上下文
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
CompletableFuture.runAsync(() -> {
// 2. 子线程中设置上下文 (第二个参数 true 表示放入 InheritableThreadLocal)
RequestContextHolder.setRequestAttributes(attributes, true);
try {
// 3. 发起 Feign 调用
userFeignClient.getUserInfo();
} finally {
// 4. 清理,防止内存泄漏
RequestContextHolder.resetRequestAttributes();
}
});
解决思路 2:配置 Spring 异步线程池装饰器(TaskDecorator)
如果你使用的是 @Async,可以配置一个自定义的线程池装饰器,让框架自动帮你拷贝上下文。
import org.springframework.core.task.TaskDecorator;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
public class ContextCopyingDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 提取主线程上下文
RequestAttributes context = RequestContextHolder.getRequestAttributes();
return () -> {
try {
// 绑定到子线程
RequestContextHolder.setRequestAttributes(context);
runnable.run();
} finally {
// 执行完毕清除
RequestContextHolder.resetRequestAttributes();
}
};
}
}
然后在线程池配置中应用它:
@Bean("asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// ... 其他配置
executor.setTaskDecorator(new ContextCopyingDecorator()); // 设置装饰器
executor.initialize();
return executor;
}
总结建议
- 常规情况:直接使用 方案一(实现
RequestInterceptor) 遍历请求头并透传。 - 务必过滤:在拦截器中记得过滤掉
Content-Length和Host。 - 异步场景:如果项目中有异步调用 Feign 的情况,必须配合
TaskDecorator或手动跨线程传递RequestContextHolder。