我在网关层加了过滤器,如果当前用户携带token,那么通过token查询对应用户的id,并放到请求头,现在用户请求进来,打在了userservice服务器上,然后他请求了上传头像,这个时候userservice服务器会通过Feign调用oss服务器,这个时候oss接收到的信息还会带着请求头里的token吗
一、一句话核心结论
默认情况下,OSS服务收不到原请求里的token和用户id。就像学生把写着学号的纸条贴在书包上,交给教务处;教务处叫小助手去档案柜办事,小助手默认不会主动把纸条抄下来带过去,档案柜自然看不到。
二、用你熟悉的学校类比,讲透底层逻辑
为什么默认带不过去?(专业原理)
这里有两个完全独立的HTTP请求,上下文是隔离的:
入站请求:用户浏览器 → 网关 → User服务。这个请求里带着网关放的token、用户id等请求头,存在于User服务的当前请求上下文中。
出站请求: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);
}
}优缺点
四、解决方案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-Length、Host等敏感请求头,导致Feign请求体长度不匹配、请求异常;传递了不需要的请求头,有安全风险,还可能被下游服务误解析。
✅ 最佳实践1:白名单机制,只传递需要的请求头
就像上面的拦截器代码,只定义Authorization、X-User-Id等必须的请求头,精准传递,避免多余问题。
❌ 反面案例2:线程池/异步场景下,请求头丢失
// 错误写法:异步线程里调用Feign,会丢失请求头
@Async
public void asyncUpload(MultipartFile file) {
// 异步线程里,RequestContextHolder.getRequestAttributes() 会返回null
fileFeignApi.uploadAvatar(file);
}问题:
RequestContextHolder底层是用ThreadLocal存储请求上下文的,异步线程里拿不到主线程的ThreadLocal数据,导致拦截器拿不到请求,请求头丢失。
✅ 最佳实践2:使用TransmittableThreadLocal解决异步场景
引入依赖(解决父子线程ThreadLocal传递问题)
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>自定义请求上下文持有器,替换默认的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();
}
}编写过滤器,提前把请求上下文放到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();
}
}
}拦截器里从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;
}
}七、最终总结
默认情况:Feign不会自动传递入站请求的请求头,OSS服务收不到token和用户id;
临时场景:用
@RequestHeader手动传递请求头,简单直观;生产场景:用Feign的
RequestInterceptor全局拦截器+白名单机制,一劳永逸,是最佳实践;异步场景:必须用
TransmittableThreadLocal替换默认的ThreadLocal,避免请求头丢失;核心原则:只传递必须的请求头,不要全量传递,避免安全问题和请求异常。