基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

微服务之间通过 Feign 调用时,如何实现请求头(如 Token、Session)的透传?

知识点图片

在微服务架构中,由于 Feign 客户端在发起远程调用时,实际上是构建了一个全新的 HTTP 请求,因此默认情况下,原始请求中的 Header(如 Token、Session、Cookie 等)会丢失。

要实现 Header 的透传,最常用且优雅的方式是使用 Feign 的 RequestInterceptor(请求拦截器)。此外,处理异步调用时的上下文丢失问题也是关键。

以下是完整的实现方案及避坑指南:


方案一:使用全局拦截器(最常用,推荐)

通过实现 feign.RequestInterceptor 接口,在 Feign 发起请求前拦截并修改请求,将原始请求的 Header 复制到新请求中。

1. 编写拦截器代码

java
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);
                }
            }
        }
    }
}

注意:

  1. 上面的 @Configuration 会让该拦截器对所有 FeignClient 生效。
  2. 千万不要透传 Content-Length,因为 Feign 构建的新请求 body 大小可能和原请求不同,透传会导致目标服务解析报文阻塞或报错。

2. 局部生效(可选)

如果你只想让某个特定的 Feign 客户端透传 Header,可以去掉上面的 @Configuration 注解,然后在 @FeignClient 注解中显式指定:

java
@FeignClient(name = "user-service", configuration = FeignRequestInterceptor.class)
public interface UserFeignClient {
    // ...
}

方案二:方法参数显式传递(适合个别接口)

如果你只有极个别接口需要传递 Token,或者不想使用拦截器,可以直接使用 @RequestHeader 注解在 Feign 接口定义处接收。

java
@FeignClient(name = "user-service")
public interface UserFeignClient {

    @GetMapping("/api/user/info")
    UserInfo getUserInfo(@RequestHeader("Authorization") String token);
}

调用处:

java
// 在 Service 层手动获取 Token 并传递
String token = request.getHeader("Authorization");
UserInfo info = userFeignClient.getUserInfo(token);

缺点:对业务代码有侵入性,每个方法都要加参数,不适合全局使用。


⚠️ 核心痛点:异步调用导致的 Header 丢失问题

问题描述:
在方案一中,我们使用了 RequestContextHolder.getRequestAttributes()。由于 RequestContextHolder 底层是基于 ThreadLocal 实现的,如果你在异步线程(如 @AsyncCompletableFuture、Hystrix 的线程池隔离模式)中发起 Feign 调用,由于线程切换,将获取不到 ServletRequestAttributes,导致报空指针或透传失败。

解决异步丢失的方案:

解决思路 1:主线程向子线程手动传递

在提交异步任务之前,将上下文提取出来,传给子线程。

java
// 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,可以配置一个自定义的线程池装饰器,让框架自动帮你拷贝上下文。

java
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();
            }
        };
    }
}

然后在线程池配置中应用它:

java
@Bean("asyncExecutor")
public Executor asyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    // ... 其他配置
    executor.setTaskDecorator(new ContextCopyingDecorator()); // 设置装饰器
    executor.initialize();
    return executor;
}

总结建议

  1. 常规情况:直接使用 方案一(实现 RequestInterceptor 遍历请求头并透传。
  2. 务必过滤:在拦截器中记得过滤掉 Content-LengthHost
  3. 异步场景:如果项目中有异步调用 Feign 的情况,必须配合 TaskDecorator 或手动跨线程传递 RequestContextHolder
00:00
00:00