前言
在微服务架构里,外部请求和内部服务调用的认证逻辑往往不一样。
比如外部用户访问接口时,必须用 user_token
来校验身份;而服务之间(比如服务 a 调用服务 b)只需要用 client_token
来表明这是内部调用,不必再走一遍复杂的用户认证。
但是问题来了:
用户带着 access_token
请求服务 a,a 会先过 spring security 的过滤器。这个过滤器里我们要远程调用服务 b 验证 access_token
,结果 feign 请求又被 security 拦了一次,导致认证死循环。
那要怎么破?本文就来聊聊这个场景的实战解法。
需求场景分析
我们先把需求捋清楚:
1.外部请求:
- 必须携带
user_token
- 通过服务 a 时,spring security 要走用户认证逻辑
2.服务间调用:
- feign 调用必须带
client_token
- 服务 b 收到请求时,只要验证这个
client_token
就行,不必再走用户认证
3.问题点:
当 a 的 security 过滤器里用 feign 调服务 b 验证 access_token
时,这个请求也会被拦截,导致无限套娃
常见坑点
- 没区分 token 类型:用户 token 和 client token 混在一起,所有请求都走同一套拦截逻辑。
- feign 默认带全局拦截:spring security 的
onceperrequestfilter
会作用于所有请求,包括 feign 发起的内部请求。 - 死循环风险:a 调 b 校验 token,b 又拦截成用户请求 → 又要去 a 验证 → 死循环。
所以我们必须设计一套机制,让外部请求和内部调用走不同的认证逻辑。
解决思路
1.区分两类请求:
- 用户请求:带
authorization: bearer user_token
- 内部调用:带
x-client-token: client_token
2.spring security 配置双认证链:
- 用户请求必须走 jwt 校验逻辑
- 内部调用只校验
client_token
,绕过用户认证
3.feign 请求统一加 client_token:
用 requestinterceptor
给 feign 请求自动加 x-client-token
代码实战
下面用 spring boot 3.x 写一个简化版 demo,演示完整流程。
1. feign 请求加上 client_token
import feign.requestinterceptor; import feign.requesttemplate; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; @configuration public class feignconfig { @bean public requestinterceptor clienttokeninterceptor() { return (requesttemplate template) -> { // 模拟内部服务调用的 client_token,可以从配置中心或 vault 获取 template.header("x-client-token", "my-client-token-123"); }; } }
这样每次服务 a 调用服务 b,都会带上 x-client-token
。
2. 自定义认证过滤器
我们需要两个过滤器,一个处理用户 token,一个处理 client token。
import jakarta.servlet.filterchain; import jakarta.servlet.servletexception; import jakarta.servlet.http.httpservletrequest; import jakarta.servlet.http.httpservletresponse; import org.springframework.security.authentication.usernamepasswordauthenticationtoken; import org.springframework.security.core.context.securitycontextholder; import org.springframework.security.web.authentication.webauthenticationdetailssource; import org.springframework.web.filter.onceperrequestfilter; import java.io.ioexception; public class clienttokenfilter extends onceperrequestfilter { private final string client_token = "my-client-token-123"; @override protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain filterchain) throws servletexception, ioexception { string clienttoken = request.getheader("x-client-token"); if (clienttoken != null && clienttoken.equals(client_token)) { // 直接认证为内部服务角色 usernamepasswordauthenticationtoken authentication = new usernamepasswordauthenticationtoken("internal-service", null, null); authentication.setdetails(new webauthenticationdetailssource().builddetails(request)); securitycontextholder.getcontext().setauthentication(authentication); } filterchain.dofilter(request, response); } }
这里的逻辑很简单:
- 如果请求头有
x-client-token
并且正确 → 直接放行,视为内部请求 - 没有的话就交给后面的用户认证逻辑去处理
3. security 配置双认证逻辑
import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.security.config.annotation.web.builders.httpsecurity; import org.springframework.security.web.securityfilterchain; import org.springframework.security.web.authentication.usernamepasswordauthenticationfilter; @configuration public class securityconfig { @bean public securityfilterchain securityfilterchain(httpsecurity http) throws exception { http.csrf(csrf -> csrf.disable()); http.authorizehttprequests(auth -> auth .requestmatchers("/public/**").permitall() .anyrequest().authenticated() ); // client token filter 优先级要高于 user token filter http.addfilterbefore(new clienttokenfilter(), usernamepasswordauthenticationfilter.class); http.addfilterbefore(new usertokenfilter(), usernamepasswordauthenticationfilter.class); return http.build(); } }
这里我们把 clienttokenfilter
放在前面,这样内部调用会优先匹配,不会再进入用户认证逻辑。
4. 用户 token 认证
public class usertokenfilter extends onceperrequestfilter { @override protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain filterchain) throws servletexception, ioexception { string authheader = request.getheader("authorization"); if (authheader != null && authheader.startswith("bearer ")) { string usertoken = authheader.substring(7); // todo: 调用服务 b 验证 user_token,有效的话设置用户上下文 // 这里省略 jwt 验证逻辑 } filterchain.dofilter(request, response); } }
这样就能保证:
- 外部请求 →
authorization: bearer user_token
→ 走usertokenfilter
- 内部调用 →
x-client-token: client_token
→ 走clienttokenfilter
实际场景结合
设想一下:
- 用户小明用 app 登录,请求
/api/orders
带着user_token
。 - 服务 a 收到请求,要调用服务 b 校验
user_token
。 - feign 调用时带上了
x-client-token
,所以服务 b 不会再拦截成用户请求,而是识别成内部调用。 - 服务 b 验证成功后返回结果,a 才继续执行业务逻辑。
这样就避免了死循环,既保证了安全性,又把内部调用和外部请求分开了。
总结
这个问题的核心是 区分外部请求和内部调用的认证链路:
- 给 feign 调用统一加
client_token
- 在 security 里加一个
clienttokenfilter
,专门处理内部请求 - 用户请求继续走 jwt 或 access_token 的逻辑
- 通过过滤器顺序,避免 feign 调用再触发用户认证,彻底解决死循环
到此这篇关于浅析springboot3.x 如何避免内部服务调用被重复拦截的文章就介绍到这了,更多相关springboot内部服务调用内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论