我在网关层加了过滤器,如果当前用户携带token,那么通过token查询对应用户的id,并放到请求头,现在用户请求进来,打在了userservice服务器上,然后他请求了上传头像,这个时候userservice服务器会通过Feign调用oss服务器,这个时候oss接收到的信息还会带着请求头里的token吗

一、一句话核心结论

默认情况下,OSS服务收不到原请求里的token和用户id。就像学生把写着学号的纸条贴在书包上,交给教务处;教务处叫小助手去档案柜办事,小助手默认不会主动把纸条抄下来带过去,档案柜自然看不到。


二、用你熟悉的学校类比,讲透底层逻辑

角色

对应技术

核心动作

正门保安

Gateway网关

检查学生的学生证(token),把学生学号(用户id)写在纸条上,贴在学生的书包上(放入站请求的请求头

学生

前端/用户请求

带着书包(请求头)进入学校,找到教务处办事

教务处

User服务

收到学生的请求,要给学生存头像到档案柜,于是叫小助手(Feign)去办事

小助手

OpenFeign

教务处给它什么,它就带什么;默认不会主动把学生书包上的纸条抄下来,贴到自己的公文包上

档案柜

OSS服务

只能看到小助手公文包里的东西,看不到学生书包上的纸条

为什么默认带不过去?(专业原理)

这里有两个完全独立的HTTP请求,上下文是隔离的:

  1. 入站请求:用户浏览器 → 网关 → User服务。这个请求里带着网关放的token、用户id等请求头,存在于User服务的当前请求上下文中。

  2. 出站请求:User服务 → Feign → OSS服务。这是Feign全新发起的一个HTTP请求,和上面的入站请求没有任何关联,Feign不会自动复制入站请求的请求头到出站请求里。


三、解决方案1:手动传递(简单临时场景)

如果只是偶尔需要传递请求头,可以在Feign接口里手动指定要传递的请求头,适合简单场景。

代码实现

1. Feign接口手动添加请求头参数

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@FeignClient(name = "lifestream-oss", path = "/file", fallback = FileFeignApiFallback.class)
public interface FileFeignApi {

    /**
     * 上传头像接口,手动指定要传递的请求头
     * @param file 头像文件
     * @param token 从入站请求里拿到的token,手动传给Feign
     * @param userId 从入站请求里拿到的用户id,手动传给Feign
     * @return 上传结果
     */
    @PostMapping("/uploadAvatar")
    String uploadAvatar(
            @RequestParam("file") MultipartFile file,
            @RequestHeader("Authorization") String token,  // 手动指定请求头key
            @RequestHeader("X-User-Id") String userId
    );
}

2. User服务调用时手动传入请求头

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private FileFeignApi fileFeignApi;

    @PostMapping("/updateAvatar")
    public String updateAvatar(
            @RequestParam("avatar") MultipartFile avatarFile,
            @RequestHeader("Authorization") String token,  // 拿到入站请求的token
            @RequestHeader("X-User-Id") String userId     // 拿到网关放的用户id
    ) {
        // 手动把请求头传给Feign
        return fileFeignApi.uploadAvatar(avatarFile, token, userId);
    }
}

优缺点

优点

缺点

简单直观,精准控制要传递的请求头

每个Feign接口都要手动加参数,代码冗余,维护麻烦

不会传递多余的请求头,安全性高

新增需要传递的请求头时,所有接口都要修改


四、解决方案2:全局拦截器(最佳实践,推荐生产使用)

通过Feign的RequestInterceptor拦截器,自动把入站请求里的指定请求头,复制到所有Feign发起的出站请求里,一劳永逸,不用修改任何业务代码。

这就像给小助手定了一个规矩:每次出门办事,都要把教务处收到的纸条(指定请求头)抄下来,贴到自己的公文包里。

代码实现

1. 编写Feign全局拦截器

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;

/**
 * Feign全局请求拦截器:自动传递入站请求的指定请求头
 */
@Configuration
public class FeignHeaderInterceptor implements RequestInterceptor {

    // 定义需要传递的请求头白名单(只传需要的,不要全量传递!)
    private static final String[] ALLOWED_HEADERS = {
            "Authorization",  // token
            "X-User-Id"       // 网关放的用户id
    };

    @Override
    public void apply(RequestTemplate requestTemplate) {
        // 1. 获取当前的入站请求
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return;
        }
        HttpServletRequest request = attributes.getRequest();

        // 2. 遍历白名单里的请求头,复制到Feign的出站请求里
        for (String headerName : ALLOWED_HEADERS) {
            String headerValue = request.getHeader(headerName);
            if (headerValue != null) {
                // 把请求头放到Feign的请求模板里
                requestTemplate.header(headerName, headerValue);
            }
        }
    }
}

2. 让拦截器生效(两种方式,选一种即可)

  • 方式1:拦截器加了@Configuration注解,Spring会自动扫描生效,无需额外配置。

  • 方式2:如果拦截器没加注解,在启动类的@EnableFeignClients里指定配置:

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients(defaultConfiguration = FeignHeaderInterceptor.class) // 指定全局配置
    public class UserServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(UserServiceApplication.class, args);
        }
    }

3. 业务代码无需任何修改

原来的代码完全不用动,Feign会自动带上请求头:

// Feign接口不用加任何请求头参数
@PostMapping("/uploadAvatar")
String uploadAvatar(@RequestParam("file") MultipartFile file);

// 调用时也不用手动传请求头
@PostMapping("/updateAvatar")
public String updateAvatar(@RequestParam("avatar") MultipartFile avatarFile) {
    // 拦截器会自动把token、用户id带过去
    return fileFeignApi.uploadAvatar(avatarFile);
}

五、避坑指南(反面案例+最佳实践)

反面案例1:全量传递所有请求头

// 错误写法:遍历所有请求头,全量传递
@Override
public void apply(RequestTemplate requestTemplate) {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if (attributes == null) return;
    HttpServletRequest request = attributes.getRequest();
    
    // 错误:全量传递所有请求头,会引发很多问题
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) {
        String headerName = headerNames.nextElement();
        requestTemplate.header(headerName, request.getHeader(headerName));
    }
}

问题

  • 会传递Content-LengthHost等敏感请求头,导致Feign请求体长度不匹配、请求异常;

  • 传递了不需要的请求头,有安全风险,还可能被下游服务误解析。

最佳实践1:白名单机制,只传递需要的请求头

就像上面的拦截器代码,只定义AuthorizationX-User-Id等必须的请求头,精准传递,避免多余问题。


反面案例2:线程池/异步场景下,请求头丢失

// 错误写法:异步线程里调用Feign,会丢失请求头
@Async
public void asyncUpload(MultipartFile file) {
    // 异步线程里,RequestContextHolder.getRequestAttributes() 会返回null
    fileFeignApi.uploadAvatar(file);
}

问题

RequestContextHolder底层是用ThreadLocal存储请求上下文的,异步线程里拿不到主线程的ThreadLocal数据,导致拦截器拿不到请求,请求头丢失。

最佳实践2:使用TransmittableThreadLocal解决异步场景

  1. 引入依赖(解决父子线程ThreadLocal传递问题)

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.2</version>
</dependency>
  1. 自定义请求上下文持有器,替换默认的ThreadLocal

import com.alibaba.ttl.TransmittableThreadLocal;
import org.springframework.web.context.request.RequestAttributes;

public class RequestContextHolderUtil {
    // 使用TransmittableThreadLocal,支持父子线程传递
    private static final ThreadLocal<RequestAttributes> REQUEST_HOLDER = new TransmittableThreadLocal<>();

    public static void setRequestAttributes(RequestAttributes attributes) {
        REQUEST_HOLDER.set(attributes);
    }

    public static RequestAttributes getRequestAttributes() {
        return REQUEST_HOLDER.get();
    }

    public static void remove() {
        REQUEST_HOLDER.remove();
    }
}
  1. 编写过滤器,提前把请求上下文放到TTL里

import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class RequestContextFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            // 把请求上下文放到TTL里,支持异步传递
            RequestContextHolderUtil.setRequestAttributes(RequestContextHolder.getRequestAttributes());
            filterChain.doFilter(request, response);
        } finally {
            RequestContextHolderUtil.remove();
        }
    }
}
  1. 拦截器里从TTL里拿请求上下文

@Override
public void apply(RequestTemplate requestTemplate) {
    // 从自定义的TTL持有器里拿请求上下文,异步场景也能拿到
    RequestAttributes attributes = RequestContextHolderUtil.getRequestAttributes();
    if (attributes == null) {
        return;
    }
    HttpServletRequest request = ((ServletRequestAttributes) attributes).getRequest();
    
    // 后续复制请求头的逻辑不变
    for (String headerName : ALLOWED_HEADERS) {
        String headerValue = request.getHeader(headerName);
        if (headerValue != null) {
            requestTemplate.header(headerName, headerValue);
        }
    }
}

六、验证方法:怎么确认OSS服务收到了请求头?

在OSS服务的Controller里,打印接收到的请求头,即可验证:

@RestController
@RequestMapping("/file")
public class FileController {

    @PostMapping("/uploadAvatar")
    public String uploadAvatar(
            @RequestParam("file") MultipartFile file,
            @RequestHeader(value = "Authorization", required = false) String token,
            @RequestHeader(value = "X-User-Id", required = false) String userId
    ) {
        // 打印接收到的请求头,验证是否传递成功
        System.out.println("收到的token:" + token);
        System.out.println("收到的用户id:" + userId);
        System.out.println("收到的文件:" + file.getOriginalFilename());
        
        return "头像上传成功,用户id:" + userId;
    }
}

七、最终总结

  1. 默认情况:Feign不会自动传递入站请求的请求头,OSS服务收不到token和用户id;

  2. 临时场景:用@RequestHeader手动传递请求头,简单直观;

  3. 生产场景:用Feign的RequestInterceptor全局拦截器+白名单机制,一劳永逸,是最佳实践;

  4. 异步场景:必须用TransmittableThreadLocal替换默认的ThreadLocal,避免请求头丢失;

  5. 核心原则:只传递必须的请求头,不要全量传递,避免安全问题和请求异常。

两块二每分钟