ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringCloud 微服务 Feign 调用 Token 透传:拦截器配置与验证

SpringCloud 微服务 Feign 调用 Token 透传:拦截器配置与验证 1. 为什么 Feign 调用下游服务总是 401在 SpringCloud 多微服务项目里这个问题几乎每个团队都会踩一次前端带着X-Access-Token请求 A 服务A 服务校验通过然后 A 通过 Feign 调用 B 服务B 服务的拦截器一查请求头发现 Token 是空的直接返回 401 或者鉴权失败。前端看到的现象就是「接口偶尔报错」「跨模块调用失败」但单独测 B 服务又是好的。根因其实不复杂。Feign 默认只会把方法参数、RequestHeader显式声明的头、以及部分编码后的 body 转发出去它不会自动把当前线程里HttpServletRequest的请求头复制到新的 HTTP 请求上。也就是说A 服务收到请求时线程上下文里确实有 Token但 Feign 发起远程调用时是另起一个RequestTemplate这个模板默认是「干净」的Token 自然就丢了。要解决它核心就一件事在 Feign 发出请求之前把当前请求上下文里的 Token 塞进RequestTemplate。SpringCloud OpenFeign 提供了RequestInterceptor这个扩展点专门干这个。下面我从拦截器视角把可复制的配置骨架、Feign 声明方式、以及怎么用日志和断点验证下游真的拿到了 Token一步步拆开讲。适合谁看正在用 SpringCloud OpenFeign 做微服务拆分、下游服务有统一鉴权拦截器、并且遇到了跨服务调用鉴权失败的开发者。读完你能直接抄走一套RequestInterceptor配置并知道怎么确认它生效。2. 前置准备TaoToken 与调用链上下文在写拦截器之前先把「Token 从哪来、要传到哪去」这条链路理清楚。很多同学配置写对了但没生效问题往往出在上下文丢失而不是拦截器本身。我这边做多模型服务编排和 Agent 调用时习惯把模型侧的密钥、调用凭证统一放在 TaoToken 上管理业务微服务只持有自己的业务 Token模型调用凭证通过网关或配置中心下发。这样 Feign 透传的就只是业务鉴权 Token职责清晰。TaoToken 官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 需要生成调用凭证时在控制台操作即可。回到 Feign 透传本身你需要确认三件事第一调用端和被调用端用的是同一套 Token 校验规则比如都用X-Access-Token这个头名或者都用token。头名不一致是最常见的「配了但没用」原因。第二调用发生在有 HTTP 请求上下文的线程里。如果你在Async、线程池、或者 MQ 消费线程里发起 Feign 调用RequestContextHolder.getRequestAttributes()会返回 null这时候就得走「无登录态」分支从UserTokenContext或TenantContext里取。第三被调用端的拦截器确实读的是你传过去的那个头。有的项目网关层做了头名改写Feign 传token网关改成X-Access-Token下游读的又是另一个名字链路就断了。提示先把「头名统一」这件事定下来再写拦截器能省掉一半排查时间。3. 可复制的 RequestInterceptor 配置骨架下面这套配置是我在 jeecg 系 SpringCloud 项目里实际用过的结构做了精简去掉了和透传无关的编解码器部分只保留 Token 透传核心。你可以直接放进自己的FeignXxxConfiguration类里。import feign.RequestInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; 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; Configuration public class FeignTokenConfiguration { private static final Logger log LoggerFactory.getLogger(FeignTokenConfiguration.class); public static final String HEADER_TOKEN X-Access-Token; public static final String HEADER_TENANT tenant-id; Bean public RequestInterceptor tokenRelayInterceptor() { return requestTemplate - { ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes ! null) { HttpServletRequest request attributes.getRequest(); String token request.getHeader(HEADER_TOKEN); if (token null || token.isEmpty()) { token request.getParameter(token); } if (token ! null !token.isEmpty()) { requestTemplate.header(HEADER_TOKEN, token); } String tenantId request.getHeader(HEADER_TENANT); if (tenantId null || tenantId.isEmpty()) { tenantId request.getParameter(HEADER_TENANT); } if (tenantId ! null !tenantId.isEmpty()) { requestTemplate.header(HEADER_TENANT, tenantId); } log.debug(Feign relay uri{}, tokenPresent{}, tenantPresent{}, request.getRequestURI(), token ! null !token.isEmpty(), tenantId ! null !tenantId.isEmpty()); } else { log.warn(Feign relay: no ServletRequestAttributes, skip token relay); } }; } }几个关键点解释一下。RequestContextHolder.getRequestAttributes()拿的是当前线程绑定的请求上下文Feign 调用如果发生在 Controller 处理线程里这里一定拿得到。requestTemplate.header(name, value)是覆盖式写入同名头会被替换不会重复堆叠。日志里我只打印「是否存在」而不打印 Token 明文避免凭证泄漏到日志系统。如果你确实需要在无请求上下文时也能透传比如定时任务里调下游可以加一个兜底分支从 ThreadLocal 里取} else { String fallbackToken UserTokenContext.getToken(); if (fallbackToken ! null !fallbackToken.isEmpty()) { requestTemplate.header(HEADER_TOKEN, fallbackToken); } String fallbackTenant TenantContext.getTenant(); if (fallbackTenant ! null !fallbackTenant.isEmpty()) { requestTemplate.header(HEADER_TENANT, fallbackTenant); } }注意UserTokenContext和TenantContext是 jeecg 自带的工具类你自己的项目里换成对应的上下文持有类即可。核心逻辑不变有请求上下文就从请求头取没有就从 ThreadLocal 取。4. Feign 声明与配置绑定方式拦截器写好了得让它挂到具体的 FeignClient 上。有两种绑定方式作用范围不同选错了会出现「有的接口透传了、有的没透传」。第一种是FeignClient上指定configuration只对这个客户端生效FeignClient( value ServiceNameConstants.SERVICE_TAXATION, configuration FeignTokenConfiguration.class, fallbackFactory TaxationFallback.class ) public interface TaxationApi { GetMapping(value /api/tax/hello) String callHello(); }第二种是全局配置在启动类或配置类上加EnableFeignClients(defaultConfiguration FeignTokenConfiguration.class)所有 FeignClient 都生效。全局方式省事但如果某个下游服务不需要鉴权、或者需要传不同的头就会互相干扰。我一般推荐「全局默认 局部覆盖」全局挂一个基础透传拦截器特殊客户端单独指定自己的 configuration。这样既不会漏又能按需定制。还有一个容易忽略的点configuration指定的类不要加Configuration注解否则它会被 Spring 主上下文扫描到变成全局 Bean导致所有 FeignClient 都应用这个配置甚至引发 Bean 冲突。正确做法是把它放在FeignClient能扫描到、但主上下文不扫描的包路径下或者干脆不加Configuration只保留Bean方法。绑定方式作用范围适用场景注意点FeignClient(configuration...)单个客户端差异化透传配置类别被主上下文扫描defaultConfiguration全部客户端统一透传特殊客户端需覆盖全局Bean RequestInterceptor全部客户端简单项目无法按客户端区分配置类里如果同时定义了Encoder、Decoder、Logger.Level注意Scope(prototype)的使用避免多客户端共享同一个编码器实例导致状态串扰。透传场景下只保留RequestInterceptor和Logger.Level通常就够了。5. 验证请求日志与断点确认下游拿到 Token配置写完最怕的是「以为生效了」。下面这套验证动作能让你确认 Token 真的到了下游。第一步打开 Feign 的完整日志。在配置类里加Bean public feign.Logger.Level feignLoggerLevel() { return feign.Logger.Level.FULL; }同时在application.yml里把对应客户端的日志级别调到 DEBUGlogging: level: com.yourpackage.api.TaxationApi: DEBUG调用一次接口你会在调用端日志里看到 Feign 发出的请求行和请求头。重点看X-Access-Token这一行有没有值。如果这里就是空的说明拦截器没生效或者上下文没取到不用往下游找了。第二步在被调用端的拦截器preHandle里打断点或者加一行日志Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(X-Access-Token); log.info(downstream receive token present{}, uri{}, token ! null !token.isEmpty(), request.getRequestURI()); // 后续校验逻辑 return true; }如果调用端日志显示头已带上但下游日志显示token presentfalse那问题在网关或中间层检查是否有过滤器把头吃掉了。如果两边都显示有值但校验还是失败那就是 Token 解析或校验规则的问题跟透传无关了。第三步用 curl 直接打下游服务带上同样的头确认下游拦截器本身能正常校验。这一步是为了排除「下游拦截器逻辑有 bug」的干扰。curl -X GET http://localhost:8082/api/tax/hello \ -H X-Access-Token: your-token-value \ -H tenant-id: 1三步走完透传链路是否通、断在哪一环就非常清楚了。6. 本篇常见错排查错误一拦截器配了但 Token 还是空。最常见原因是configuration类被Configuration注解后进了主上下文或者包路径被ComponentScan扫到导致它没按预期绑定到指定客户端。检查方式是看启动日志里这个配置类是不是被当成全局 Bean 加载了。错误二RequestContextHolder返回 null。说明 Feign 调用不在 HTTP 请求线程里比如异步线程、定时任务、MQ 消费。这时候要么把上下文手动传进去要么走 ThreadLocal 兜底分支。别硬在拦截器里new一个 request那是拿不到真实头的。错误三头名不一致。调用端传token下游读X-Access-Token或者网关做了头名改写。统一头名或者在拦截器里同时写多个头名做兼容。错误四Token 被网关或过滤器覆盖。有些网关会重置请求头Feign 传过去的 Token 被清掉。检查网关的过滤器链确认没有对X-Access-Token做 remove 或 rewrite。错误五循环依赖导致配置类加载失败。如果配置类里注入了其他 Bean而那个 Bean 又依赖 FeignClient可能形成循环。透传拦截器尽量保持无依赖只做纯头复制能避免这类问题。错误六多客户端共享拦截器实例导致状态串扰。如果拦截器里存了可变状态多个客户端并发调用会互相影响。拦截器写成无状态的 lambda 或纯函数式最安全。注意排查时优先看调用端 Feign 日志再看下游接收日志最后看网关。顺序反了容易在无关环节浪费时间。如果你在验证模型调用链路、需要生成和管理调用凭证可以在 TaoToken 控制台创建 API Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 接入细节参考文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。需要长期跑编码类 Agent、把 Feign 透传和模型调用串起来的场景可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。想先验证模型对话是否正常直接进模型对话页https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。密钥管理入口在 API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。最后留一个我踩过的坑拦截器里requestTemplate.header(name, value)的 value 如果是 nullFeign 会抛 NPE所以每个头写入前都要判空。这个报错堆栈不会直接指向你的拦截器而是指向 Feign 的编码阶段第一次遇到很容易懵。判空加上问题就没了。
返回列表