淺析SpringBoot3.x 如何避免內(nèi)部服務(wù)調(diào)用被重復(fù)攔截
前言
在微服務(wù)架構(gòu)里,外部請求和內(nèi)部服務(wù)調(diào)用的認(rèn)證邏輯往往不一樣。
比如外部用戶訪問接口時(shí),必須用 user_token 來校驗(yàn)身份;而服務(wù)之間(比如服務(wù) A 調(diào)用服務(wù) B)只需要用 client_token 來表明這是內(nèi)部調(diào)用,不必再走一遍復(fù)雜的用戶認(rèn)證。
但是問題來了:
用戶帶著 access_token 請求服務(wù) A,A 會先過 Spring Security 的過濾器。這個(gè)過濾器里我們要遠(yuǎn)程調(diào)用服務(wù) B 驗(yàn)證 access_token,結(jié)果 Feign 請求又被 Security 攔了一次,導(dǎo)致認(rèn)證死循環(huán)。
那要怎么破?本文就來聊聊這個(gè)場景的實(shí)戰(zhàn)解法。
需求場景分析
我們先把需求捋清楚:
1.外部請求:
- 必須攜帶
user_token - 通過服務(wù) A 時(shí),Spring Security 要走用戶認(rèn)證邏輯
2.服務(wù)間調(diào)用:
- Feign 調(diào)用必須帶
client_token - 服務(wù) B 收到請求時(shí),只要驗(yàn)證這個(gè)
client_token就行,不必再走用戶認(rèn)證
3.問題點(diǎn):
當(dāng) A 的 Security 過濾器里用 Feign 調(diào)服務(wù) B 驗(yàn)證 access_token 時(shí),這個(gè)請求也會被攔截,導(dǎo)致無限套娃
常見坑點(diǎn)
- 沒區(qū)分 token 類型:用戶 token 和 client token 混在一起,所有請求都走同一套攔截邏輯。
- Feign 默認(rèn)帶全局?jǐn)r截:Spring Security 的
OncePerRequestFilter會作用于所有請求,包括 Feign 發(fā)起的內(nèi)部請求。 - 死循環(huán)風(fēng)險(xiǎn):A 調(diào) B 校驗(yàn) token,B 又?jǐn)r截成用戶請求 → 又要去 A 驗(yàn)證 → 死循環(huán)。
所以我們必須設(shè)計(jì)一套機(jī)制,讓外部請求和內(nèi)部調(diào)用走不同的認(rèn)證邏輯。
解決思路
1.區(qū)分兩類請求:
- 用戶請求:帶
Authorization: Bearer user_token - 內(nèi)部調(diào)用:帶
X-Client-Token: client_token
2.Spring Security 配置雙認(rèn)證鏈:
- 用戶請求必須走 JWT 校驗(yàn)邏輯
- 內(nèi)部調(diào)用只校驗(yàn)
client_token,繞過用戶認(rèn)證
3.Feign 請求統(tǒng)一加 client_token:
用 RequestInterceptor 給 Feign 請求自動(dòng)加 X-Client-Token
代碼實(shí)戰(zhàn)
下面用 Spring Boot 3.x 寫一個(gè)簡化版 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) -> {
// 模擬內(nèi)部服務(wù)調(diào)用的 client_token,可以從配置中心或 Vault 獲取
template.header("X-Client-Token", "my-client-token-123");
};
}
}
這樣每次服務(wù) A 調(diào)用服務(wù) B,都會帶上 X-Client-Token。
2. 自定義認(rèn)證過濾器
我們需要兩個(gè)過濾器,一個(gè)處理用戶 token,一個(gè)處理 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)) {
// 直接認(rèn)證為內(nèi)部服務(wù)角色
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并且正確 → 直接放行,視為內(nèi)部請求 - 沒有的話就交給后面的用戶認(rèn)證邏輯去處理
3. Security 配置雙認(rèn)證邏輯
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 優(yōu)先級要高于 user token filter
http.addFilterBefore(new ClientTokenFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new UserTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
這里我們把 ClientTokenFilter 放在前面,這樣內(nèi)部調(diào)用會優(yōu)先匹配,不會再進(jìn)入用戶認(rèn)證邏輯。
4. 用戶 token 認(rèn)證
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: 調(diào)用服務(wù) B 驗(yàn)證 user_token,有效的話設(shè)置用戶上下文
// 這里省略 JWT 驗(yàn)證邏輯
}
filterChain.doFilter(request, response);
}
}
這樣就能保證:
- 外部請求 →
Authorization: Bearer user_token→ 走UserTokenFilter - 內(nèi)部調(diào)用 →
X-Client-Token: client_token→ 走ClientTokenFilter
實(shí)際場景結(jié)合
設(shè)想一下:
- 用戶小明用 app 登錄,請求
/api/orders帶著user_token。 - 服務(wù) A 收到請求,要調(diào)用服務(wù) B 校驗(yàn)
user_token。 - Feign 調(diào)用時(shí)帶上了
X-Client-Token,所以服務(wù) B 不會再攔截成用戶請求,而是識別成內(nèi)部調(diào)用。 - 服務(wù) B 驗(yàn)證成功后返回結(jié)果,A 才繼續(xù)執(zhí)行業(yè)務(wù)邏輯。
這樣就避免了死循環(huán),既保證了安全性,又把內(nèi)部調(diào)用和外部請求分開了。
總結(jié)
這個(gè)問題的核心是 區(qū)分外部請求和內(nèi)部調(diào)用的認(rèn)證鏈路:
- 給 Feign 調(diào)用統(tǒng)一加
client_token - 在 Security 里加一個(gè)
ClientTokenFilter,專門處理內(nèi)部請求 - 用戶請求繼續(xù)走 JWT 或 access_token 的邏輯
- 通過過濾器順序,避免 Feign 調(diào)用再觸發(fā)用戶認(rèn)證,徹底解決死循環(huán)
到此這篇關(guān)于淺析SpringBoot3.x 如何避免內(nèi)部服務(wù)調(diào)用被重復(fù)攔截的文章就介紹到這了,更多相關(guān)SpringBoot內(nèi)部服務(wù)調(diào)用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mybatis-plus生成mapper擴(kuò)展文件的方法
這篇文章主要介紹了mybatis-plus生成mapper擴(kuò)展文件的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
spring-data-redis 動(dòng)態(tài)切換數(shù)據(jù)源的方法
最近遇到了一個(gè)麻煩的需求,我們需要一個(gè)微服務(wù)應(yīng)用同時(shí)訪問兩個(gè)不同的 Redis 集群,一般情況下我們會怎么處理呢,下面通過場景分析給大家介紹spring-data-redis 動(dòng)態(tài)切換數(shù)據(jù)源的方法,感興趣的朋友一起看看吧2021-08-08
Java零基礎(chǔ)教程之Windows下安裝、啟動(dòng)Tomcat服務(wù)器方法圖解(免安裝版)
這篇文章主要介紹了Windows系統(tǒng)下安裝、啟動(dòng)、注冊服務(wù)、停止 Tomcat操作的所有方法,本文通過圖文并茂的方式給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2016-09-09
Maven中生命周期深度解析與實(shí)戰(zhàn)指南
這篇文章主要為大家詳細(xì)介紹了Maven 生命周期實(shí)戰(zhàn)指南,包含核心概念、階段詳解、Spring Boot 特化場景及企業(yè)級實(shí)踐建議,希望對大家有一定的幫助2025-08-08
在jmeter的beanshell中用java獲取系統(tǒng)當(dāng)前時(shí)間的簡單實(shí)例
這篇文章介紹了在jmeter的beanshell中用java獲取系統(tǒng)當(dāng)前時(shí)間的簡單實(shí)例,有需要的朋友可以參考一下2013-09-09

