微服务Feign调用跳过认证?一文搞定内部接口白名单
做微服务改造的那阵子几乎每个团队都会碰到一个非常具体的痛点服务A通过Feign去调服务B的接口结果居然报401翻日志一看认证过不去。你说链路通不通通。代码有没有错看起来也没错。但就是卡在认证授权这一层因为原始请求的Token在服务间中转的时候没带上或者某个内部逻辑触发的调用压根没有用户Token。于是“微服务之间调用部分Feign接口忽略认证授权”这个需求就成了几乎所有Spring Cloud项目里都绕不开的一道坎。这个需求说直白点就是给内部服务间调用的部分接口开一条“特权通道”走白名单的接口不用校验Token其他接口照常走完整认证授权。它解决的是服务间调用效率、跨服务身份传递复杂度过高的问题同时也把“什么接口可以信”这个边界重新划清楚。这篇文章适合正在用Spring Cloud OpenFeign做微服务拆分、又被JWT/OAuth2认证折磨过的后端同学尤其是已经发现全链路强校验导致内部调用频繁失败、准备动手调整认证策略的人。1. 先搞清楚为什么会有“内部调用跳过认证”这种需求1.1 微服务里Feign调用的认证困局微服务拆完之后服务之间的调用变得非常频繁。一个用户下单操作可能订单服务要调库存服务库存服务要调商品服务商品服务可能要调价格服务。如果用OpenFeign做同步调用每个接口在设计时都会下意识地“复用”用户那套认证体系也就是在网关层做登录校验然后往里层服务塞Token。问题就出在这个“下意识”上。外层过来的请求带着JWT网关校验通过之后转发到订单服务这一步没问题。但订单服务用Feign去调库存服务时Feign默认并不会自动把当前请求的Header带过去尤其是Authorization这种敏感Header。很多团队第一版会写一个Feign拦截器把请求上下文里的Token复制到新请求里试图做到全链路透传。这类方案在几分钟内的演示Demo里很好用真正跑起来就发现三个躲不掉的坑有些接口是定时任务、MQ消费者等无用户上下文场景触发的压根没有Token可传。多个服务之间传递的是同一个JWTToken过期时间由最外层那个最初请求决定链路稍微一长就大面积超时失败。不同服务对用户权限要求不同某些内部接口本来就不需要关心用户是谁硬传一个Token反而让下游服务做了一堆无意义的解析和校验。我见过一个项目内部服务全链路统一用JWT强校验结果一到凌晨跑批就疯狂报错因为很多批处理任务根本没有用户身份。后来排查了两天才发现问题不是代码逻辑错了而是认证设计压根没考虑内部调用这个场景。1.2 “跳过认证”不等于裸奔很多人一听“忽略认证授权”第一反应是直接把所有服务接口的白名单全开了觉得反正内网嘛没人能打进来。这个思路非常危险。跳过认证只是跳过“用户身份校验”这一步不代表接口不需要保护。正确的做法是把“认证”和“信任边界”分开理解对外接口必须是完整链路登录态校验、Token校验、权限校验一个不少。内部接口可以走独立的信任机制比如通过网段限制、内部网关、或者单独的内部凭证来保证只有注册过的服务能访问。换句话说我们不是不设防而是换一种更适合内部场景的防法。白名单接口虽然不校验用户Token但要有明确的边界哪些服务能用、哪些IP能访问、请求头里有没有内部标识。把这些前提做好了再谈“忽略认证”才不算给自己挖坑。2. 三种主流实现思路与我的选型结论2.1 思路AFeign拦截器全链路透传上游Token这个思路最直观写一个Feign的RequestInterceptor从当前的RequestContextHolder里取出Request把Authorization Header原样拷贝到Feign的RequestTemplate里。这样做的好处是改造成本极低代码量也就十几行第一次跑通的时候你会觉得“这就完了”但它的局限性也很明显。透传Token只能解决“上游有Token”的内部调用一旦遇到并发线程、异步任务、定时器或者MQ消费者RequestContextHolder里根本取不到ServletRequest透传逻辑直接失效。而且全链路传同一个Token会让下游每个服务都去做一次JWT解析和权限判断性能和复杂度都是浪费。我在实际项目中用这种方案踩过一个更隐蔽的坑某服务用线程池并发调多个下游接口ThreadLocal里的Request在子线程里取不到代码一上线一半的调用直接NPE排查的时候一头雾水。所以我的结论是透传Token适合“保留用户身份信息给下游做鉴权和审计”的场景但不适合用来解决“内部接口能不能不校验认证”的问题。它解决的是“怎么传”不解决“要不要认”。2.2 思路B网关层为内部接口开绿灯在API网关层面路由配置里对内部接口的特殊前缀做过滤器顺序调整。比如网关接收外部请求时如果请求路径匹配/internal/**就跳过全局Token校验过滤器直接路由到下游服务。这个方案在架构上清晰认证逻辑集中在网关一层服务内部可以基本保持“无感知”。但要注意的是网关开绿灯的同时必须保证这个路径外部根本访问不到否则就相当于把一个裸奔接口暴露到公网上。大多数时候网关的/internal/**路由应该在防火墙规则里配合安全组甚至直接把服务网络和外部网络隔离。支撑团队内部多个服务之间调用时还有一个细节容易漏网关跳过了认证但下游服务可能自己还配了一套Spring Security。结果网关把请求放过去了下游服务又给你拦一道整个链路照样失败。这种时候就不是“网关开白名单”能搞定的得在服务端的认证过滤器里也做配合。其实网关方案适合网关和内部服务分层清晰、且服务级不再做二次认证的团队治理相对集中但一旦网关停机内部调用的认证也会跟着断掉。2.3 思路C服务内按路径做白名单我的最终选择最终我的选择是第三种在服务内部自己的认证过滤器/拦截器里按请求路径维护一份白名单。匹配上的路径直接跳过Token校验其他路径照常处理。为什么选它因为在微服务治理还不够标准化的团队里服务内白名单是分布在各服务里的每个服务自己控制自己对外开放的接口边界。网关统一跳闸看起来很美但你要确保所有团队都能保持一致其实很难。服务内白名单的优点是改动局部化某个服务的接口要不要对外校验该服务自己说了算。不依赖网关是否在线服务本身处理得干净。对现有Spring Security、Sa-Token之类的拦截流程侵入小就是在过滤链里加一个判断。缺点也很清楚如果项目里有好几十个服务每个服务都自己维护一份白名单散落各处不好管理。所以后来我又加了一个通用组件把白名单配置统一放在Nacos Config里下发所有服务加载同一份配置治理起来就顺多了。3. 落地实操自定义认证过滤器 内部接口白名单3.1 准备阶段技术选型和依赖版本下面的实操示例基于Spring Boot 2.7.x Spring Cloud 2021.x OpenFeign。如果你用的是Spring Boot 3.x注意javax包名需要替换成jakarta逻辑完全一致。需要引入的核心依赖dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId /dependency我建议把白名单配置放到Nacos Config里而不是写死在application.yml或者Java代码里。即使用不到Nacos也建议至少用ConfigurationProperties绑定外部配置。原因很简单白名单是运维策略的一部分可能会随着服务拆分、接口下线而调整固化在代码里就意味着每一次调整都要发版代价太高。3.2 核心配置白名单路径的加载与管理在Nacos配置里加一段security: ignore: urls: - /internal/** - /feign/inventory/** - /feign/order/** - /health/**这里有个很重要的设计建议内部接口尽量不要散落在业务路径里最好统一加前缀。我习惯用/internal/**或者/feign/**作为内部调用专用前缀这样在过滤器和网关层做匹配时都极其方便也方便后续做网络策略隔离。服务启动时通过配置类加载这些白名单Component ConfigurationProperties(prefix security.ignore) public class IgnoreUrlProperties { private ListString urls new ArrayList(); public ListString getUrls() { return urls; } public void setUrls(ListString urls) { this.urls urls; } public boolean isIgnoreUrl(String requestPath) { for (String pattern : urls) { if (AntPathMatcher.INSTANCE.match(pattern, requestPath)) { return true; } } return false; } }上面的AntPathMatcher来自于Spring的spring-core包/internal/**这种写法是标准的Ant通配符规则匹配/internal/下的任意层级路径。如果你用的是Spring Boot 3可以换成PathPatternParser不过对于这个场景AntPathMatcher完全够用。3.3 核心代码认证过滤器里的跳过逻辑下面这段是核心中的核心。以Spring Security为例在OncePerRequestFilter里判断是否命中白名单命中就直接放行不再走后续认证逻辑Component public class AuthenticationFilter extends OncePerRequestFilter { private final IgnoreUrlProperties ignoreUrlProperties; public AuthenticationFilter(IgnoreUrlProperties ignoreUrlProperties) { this.ignoreUrlProperties ignoreUrlProperties; } Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestPath request.getRequestURI(); // 命中白名单直接放行 if (ignoreUrlProperties.isIgnoreUrl(requestPath)) { filterChain.doFilter(request, response); return; } // 非白名单路径走正常Token解析和校验 String token resolveToken(request); if (token null || !validToken(token)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\unauthorized\}); return; } // 解析用户信息放入上下文 SecurityContextHolder.getContext().setAuthentication(parseAuthentication(token)); filterChain.doFilter(request, response); } }这里的resolveToken一般是从Authorization头里取出JWT也有项目习惯从Cookie里取看你自己的规范。validToken是调远程认证中心或者本地验签的逻辑。在实际项目中我不建议在过滤器里同步调远程认证中心除非你能接受每次请求增加一次RTT不然还是用本地解析JWT 公钥验签更稳妥。如果是Sa-Token则是在SaServletFilter里做类似判断或者直接使用Sa-Token提供的注解排除能力。但最通用、最容易做统一管控的还是自定义过滤器判断这一刀切法。3.4 服务间Feign调用怎么配合白名单白名单方案是服务端的事调用端Feign这边要不要做什么答案是可以不做但建议至少做两件事。第一件事内部Feign调用建议加专门的请求头比如X-Internal-Protected: true或者X-Service-Auth: 内部凭证。虽然服务端已经按路径放行了但加标识的好处是日志审计时能一眼分辨出这次请求是内部调用还是外部请求排障速度完全不一样。第二件事如果是注册中心模式下建议通过服务名而不是写死IP地址来做负载均衡这样就算白名单路径变了调用方也不需要改代码。OpenFeign这边最常见的配置是FeignClient(name inventory-service, path /internal/inventory) public interface InventoryFeignClient { GetMapping(/stock/{skuId}) StockResult getStock(PathVariable(skuId) String skuId); }这样Feign实际发出去的URL就是/internal/inventory/stock/{skuId}命中服务端白名单认证直接放行。路径前缀直接设计成白名单范围能让配置和代码两层对应关系一目了然。我在项目里就是这么约定的所有Feign接口的path必须带/internal前缀否则代码评审就不给过。这不是强制技术而是靠规范和日志把混乱扼杀在源头。3.5 更进阶的玩法把白名单做成服务发现动态映射在几十个服务的大项目里靠手写URL列表容易漏这时候可以考虑把“白名单”和“服务发现”联动起来。思路是白名单的路径前缀由服务名映射生成。例如Nacos上注册的服务名是inventory-service那么白名单自动生成/internal/inventory/**服务名变了白名单跟着变。这个方案的技术细节稍微复杂需要处理服务上下线监听但在治理层面非常有效。我当时的实现其实就是写了个监听器监听Nacos的InstancesChangeEvent动态刷新白名单配置。感兴趣的读者可以在把静态配置版本跑通后再尝试不需要一上来就做这种动态化先把主流程稳定住更关键。4. 常见问题与排查技巧实录4.1 高频问题速查表这里我把实际运维和开发过程中遇到频次最高的几个问题列出来每条都是踩过坑之后总结出来的。现象常见原因排查建议白名单配置了请求还是401过滤器执行顺序不对其他过滤器先拦了看过滤器链里是否还有额外的认证Filter确认顺序/internal/**在网关被外部访问到网关层没做二次拦截或网络策略没隔离用防火墙/安全组限制内部路径来源IPFeign调用能用浏览器直连也能用白名单逻辑依赖路径但没校验调用来源增加来源校验比如请求头必须有X-Internal-Protected动态刷新白名单不生效配置类没有加RefreshScope给IgnoreUrlProperties补充RefreshScope或在刷新时手动重置缓存白名单放行后业务代码里拿不到用户信息过滤器直接放行了没解析用户上下文根据业务需要决定是否要放行但保留一个匿名用户上下文定时任务调用白名单接口报错内部接口白名单只考虑了Feign场景定时任务没经过HTTP检查调用方式是否直接走HTTP而非Feign或将任务调用归类为另一种内部信任方式4.2 我踩过的坑与最终解法第一个坑是过滤器顺序问题。当时白名单逻辑写在自定义AuthenticationFilter里但项目里还有另一个老的SecurityFilter它也会校验Token。我俩过滤器都加了结果自定义过滤器先放行了老过滤器又给拦住了白名单形同虚设。后来解决办法是调整Order让统一的认证入口在最前面其他Filter一律依赖它处理完的上下文不在重复校验。第二个坑是网关层暴露。有段时间我们把内部接口的/internal/**路径在网关配成了放行结果安全组那边没同步更新从公网直接访问网关IP加内部路径居然能通。那属实吓人。后来我给自己定了个死规矩凡是内部白名单路径在任意入口层的安全防火墙里都必须额外再拦一道应用层的白名单只是最后一道保险不负责防外层攻击。第三个坑是Nacos刷新不生效。白名单配置放在Nacos里之后本地改了Nacos配置服务不重启就是不识别。查了源码才发现自己忘了加RefreshScope导致配置对象还是老地址的守护代理。这个挺容易漏因为Spring Cloud Config和Nacos Config的使用习惯不太一样。4.3 上线前必须检查的四个点白名单机制上线前我建议做一轮“白名单专项审查”不要等到线上出问题再回来补。我通常按下面四个维度过一遍白名单路径是否收敛是不是所有/internal/**都是内部需要的有没有多余的历史接口混在里面来源是否可追踪每个放行接口在日志里能否看到调用方服务名或者IP如果白名单接口出了问题能不能快速定位是哪个服务调过来的是否依赖网络隔离应用层白名单是否与防火墙规则联动如果所在环境没有网络隔离仅仅靠路径白名单是不够的。是否有审计兜底对于放行接口的写操作比如内部订单状态变更、库存扣减即使不做用户认证也要有审计流水方便后期复盘。这三个检查点随便拿掉一个都有可能把“内部接口的特权”变成“整个系统的薄弱口”。5. 写在最后的个人体会我之前在做这个需求的时候从最开始的“全链路强认证”到“Feign透传Token”再到最后的“部分接口白名单”反复折腾了三版才稳定下来。最大的收获不是代码怎么写而是对“认证”这件事想通了一层认证不是越严格越好而是要区分清楚信任边界。对用户请求严格认证是对的对服务之间的内部调用更合适的做法是建立一套独立的信任标识体系让服务之间有一个“自己人”的快速通道。路径白名单只是其中一种实现但它背后的思想——明确区分“外部身份认证”和“内部服务信任”——才是真正让微服务架构跑得顺的核心。最后再分享一个我后来一直保留的小习惯无论白名单配置得多好我都会在服务里加一个简单的监控指标统计白名单接口的调用频率、来源IP、成功失败率。一旦某个内部接口的调用量异常上涨监控能第一时间发现。毕竟安全和稳定这件事从来不是配置一次就能一劳永逸得靠持续观测才行。