做微服务改造的那阵子,几乎每个团队都会碰到一个非常具体的痛点:服务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可传。
- 多个服务之间传递的是同一个JWT,Token过期时间由最外层那个最初请求决定,链路稍微一长就大面积超时失败。
- 不同服务对用户权限要求不同,某些内部接口本来就不需要关心用户是谁,硬传一个Token反而让下游服务做了一堆无意义的解析和校验。
我见过一个项目,内部服务全链路统一用JWT强校验,结果一到凌晨跑批就疯狂报错,因为很多批处理任务根本没有用户身份。后来排查了两天才发现,问题不是代码逻辑错了,而是认证设计压根没考虑内部调用这个场景。
1.2 “跳过认证”不等于裸奔
很多人一听“忽略认证授权”,第一反应是直接把所有服务接口的白名单全开了,觉得反正内网嘛,没人能打进来。这个思路非常危险。
跳过认证只是跳过“用户身份校验”这一步,不代表接口不需要保护。正确的做法是把“认证”和“信任边界”分开理解:
- 对外接口,必须是完整链路:登录态校验、Token校验、权限校验一个不少。
- 内部接口,可以走独立的信任机制,比如通过网段限制、内部网关、或者单独的内部凭证来保证只有注册过的服务能访问。
换句话说,我们不是不设防,而是换一种更适合内部场景的防法。白名单接口虽然不校验用户Token,但要有明确的边界:哪些服务能用、哪些IP能访问、请求头里有没有内部标识。把这些前提做好了,再谈“忽略认证”才不算给自己挖坑。
2. 三种主流实现思路与我的选型结论
2.1 思路A:Feign拦截器全链路透传上游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> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-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 List<String> urls = new ArrayList<>(); public List<String> getUrls() { return urls; } public void setUrls(List<String> 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;charset=UTF-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/**在网关被外部访问到 | 网关层没做二次拦截或网络策略没隔离 | 用防火墙/安全组限制内部路径来源IP |
| Feign调用能用,浏览器直连也能用 | 白名单逻辑依赖路径但没校验调用来源 | 增加来源校验,比如请求头必须有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、成功失败率。一旦某个内部接口的调用量异常上涨,监控能第一时间发现。毕竟安全和稳定这件事,从来不是配置一次就能一劳永逸,得靠持续观测才行。