如何避免 OpenFeign 内部调用被 Spring Security 重复拦截
在 Spring Boot 3.x + Spring Security + OpenFeign 的环境中,当服务 A 的安全过滤器使用 OpenFeign 调用服务 B 进行 token 验证时,该内部调用又被服务 A 的安全过滤器拦截,导致认证失败。这是因为 Spring Security 的过滤器链默认处理所有传入请求(包括 OpenFeign 发起的内部调用)。关键点在于区分用户请求(带 user_token)和内部服务调用(带 client_token),并为内部调用设置“豁免”规则或单独的安全路径。
解决方案步骤
以下是完整解决方案,确保外部请求(用户)通过 user_token 认证,而内部 OpenFeign 调用使用 client_token 且不被重复拦截。方案基于 服务 A 和 服务 B 协同配置。
1. 问题原因分析
- 当用户请求到达服务 A,被 Spring Security 过滤器拦截。过滤器通过 OpenFeign 调用服务 B 时,OpenFeign 的请求 也被视为传入请求,因为过滤器链是全局的。
- 导致认证失败:OpenFeign 请求没有 user_token(可能已使用安全上下文传播),但过滤器期望它(尽管应该用 client_token)。
- 根本原因:服务 A 的安全配置没有区分“外部请求”和“内部服务调用”。需要将 OpenFeign 调用从安全过滤中排除。
2. 核心思路:使用 @PermitAll 或 RequestMatcher 排除内部路径
在服务 A 中,为所有内部 API(包括 OpenFeign 客户端调用的接口)配置一个特定路径前缀(例如 /internal/**),并在 Spring Security 中设置此路径不要求认证。
优点:
- OpenFeign 调用服务 B 的请求被定向到
/internal/** 路径,在服务 A 中被标记为“内部”,从而跳过 user_token 拦截。 - 服务 B 使用相同路径前缀处理内部调用,但要求 client_token 认证(配置在服务 B 的安全规则中)。
3. 具体实现步骤(配置服务 A 和 服务 B)
假设服务 A 的 OpenFeign 客户端调用服务 B 的验证端点(例如 /verify-token)。
步骤 1: 在服务 A 中创建内部 API 路径并配置 OpenFeign
- 定义内部路径前缀:所有内部 API 使用如
/internal/**。 - 创建 OpenFeign 客户端:调用服务 B 的验证端点时,使用
/internal/verify-token 路径,并在请求头中添加 client_token。
// 服务 A: 创建 FeignClient 接口,指向服务 B 的内部路径
@FeignClient(name = "serviceBClient", url = "http://service-b")
public interface ServiceBClient {
@PostMapping("/internal/verify-token")
boolean verifyToken(@RequestHeader("Authorization") String clientToken, @RequestBody String userToken);
}
- 添加 RequestInterceptor:自动为所有 OpenFeign 调用添加 client_token 头。
// 服务 A: 配置全局 Feign Interceptor
@Configuration
public class FeignConfig {
@Value("${security.client-token}") // 从配置读取 client_token
private String clientToken;
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
requestTemplate.header("Authorization", "Bearer " + clientToken); // 添加 Bearer client_token
};
}
}
步骤 2: 在服务 A 中配置 Spring Security 忽略内部路径
- Spring Security 配置:创建一个自定义
SecurityFilterChain,让 /internal/** 路径的请求跳过认证。 - 使用
permitAll() 允许访问,或使用 @PermitAll 在 Controller 层标记(但配置中更可靠)。
// 服务 A: 安全配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/internal/**").permitAll() // 关键:跳过所有 /internal 路径的认证
.anyRequest().authenticated() // 其他请求要求 user_token 认证
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()) // 用户 user_token 验证逻辑
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(csrf -> csrf.disable()); // 无状态服务通常需禁用 CSRF
return http.build();
}
private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthConverter() {
// 自定义 Jwt 转换器,处理 user_token 逻辑
return jwt -> {
String tokenValue = jwt.getTokenValue();
// 这里是验证 user_token 的逻辑(但在此方案中,内部调用已跳过)
return new JwtAuthenticationToken(jwt, Collections.emptyList());
};
}
}
步骤 3: 在服务 B 中配置双重安全规则
服务 B 需处理两种请求:
- 用户直接访问的路径(如
/api/**):要求 user_token 认证。 - 内部服务调用的路径(如
/internal/**):要求 client_token 认证,而不是 user_token。
- 使用多个
SecurityFilterChain:一个用于内部路径(要求 client_token),一个用于外部路径(要求 user_token)。
// 服务 B: 安全配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1) // 高优先级匹配内部路径
public SecurityFilterChain internalFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/internal/**") // 只匹配内部路径
.authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(clientTokenAuthConverter()) // 处理 client_token
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
@Order(2) // 低优先级,匹配其他路径
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // 用户访问路径
.authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(userTokenAuthConverter()) // 处理 user_token
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
// 自定义转换器处理 client_token
private Converter<Jwt, ? extends AbstractAuthenticationToken> clientTokenAuthConverter() {
return jwt -> {
String tokenValue = jwt.getTokenValue();
// 验证 client_token 逻辑,例如检查 issuer 是否为服务账户
if (!"expected-issuer-for-client".equals(jwt.getIssuer())) {
throw new JwtValidationException("Invalid client token");
}
return new JwtAuthenticationToken(jwt, Collections.emptyList());
};
}
// 自定义转换器处理 user_token
private Converter<Jwt, ? extends AbstractAuthenticationToken> userTokenAuthConverter() {
return jwt -> {
// 正常 user_token 验证逻辑
return new JwtAuthenticationToken(jwt, Collections.singletonList(new SimpleGrantedAuthority("USER")));
};
}
}
步骤 4: 在服务 B 中实现验证端点
- 在内部路径(
/internal/verify-token)提供 token 验证逻辑。这个端点被服务 A 的 OpenFeign 调用。
// 服务 B: 控制器提供验证端点
@RestController
public class TokenController {
@PostMapping("/internal/verify-token")
public boolean verifyToken(@RequestHeader("Authorization") String authHeader, @RequestBody String userToken) {
// 验证 client_token(自动由服务 B 的 internalFilterChain 处理)
// 提取和验证 user_token 的业务逻辑
return "valid".equals(userToken); // 简化示例
}
}
4. 如何避免安全上下文传播问题
- 当服务 A 的过滤器调用 OpenFeign 时,如果 SecurityContext 被传播,可能导致内部调用被误认证为“用户请求”。
- 解决方法:在调用 OpenFeign 前手动清除 SecurityContext(可选,通常不需)。
// 在服务 A 的过滤器中调用 OpenFeign 前清除上下文
public class CustomFilter extends OncePerRequestFilter {
private final ServiceBClient serviceBClient;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
// ... 处理外部请求
// 调用 OpenFeign 前清除上下文
SecurityContextHolder.clearContext();
boolean isValid = serviceBClient.verifyToken(clientToken, userToken); // 请求不会被服务 A 拦截
SecurityContext context = ...; // 恢复原始上下文(如果需要)
// 继续过滤器链
filterChain.doFilter(request, response);
}
}
5. 验证与测试
总结
- 关键点:通过路径隔离(
/internal/**)和安全配置中的 permitAll() 避免 OpenFeign 内部调用在服务 A 中被重复拦截。 - 优势:用户请求和内部调用分离;外部路径使用 user_token,内部路径使用 client_token(服务 B 处理)。
- 依赖:Spring Security 的多个
SecurityFilterChain 和 OpenFeign 的 RequestInterceptor。 - 注意:在微服务架构中,这种模式扩展性好,但需确保所有服务都采用一致的内部路径约定。最终实现了外部请求通过 user_token 认证,内部调用只通过 client_token 认证而不再触发 user_token 拦截。
一、Feign 请求添加标识头 + client_token
二、自定义过滤器:跳过内部调用的用户认证
三、Spring Security 配置中区分处理逻辑
四、自定义认证过滤器:区分 user_token 和 client_token