如何为Spring Cloud Reactive API Gateway自定义过滤器编写单元测试验证公共端点放行
Spring Cloud Gateway AuthAccountFilter 单元测试指导
一、先修复过滤器本身的逻辑缺陷
原AuthAccountFilter存在两处关键问题:
- 安全端点分支无返回值,会触发编译错误
- 公共端点判断逻辑冗余,可优化为集合维护
- 依赖注入方式不利于测试,建议改用构造注入
修复后的过滤器代码:
package gateway.filters; import com.nimbusds.jose.JWSObject; import com.nimbusds.jose.shaded.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.ResponseStatusException; import reactor.core.publisher.Mono; import java.text.ParseException; import java.util.Set; @Component public class AuthAccountFilter extends AbstractGatewayFilterFactory<AuthAccountFilter.Config> { private static final Logger LOGGER = LoggerFactory.getLogger(AuthAccountFilter.class); private static final Set<String> PUBLIC_ENDPOINT_KEYWORDS = Set.of("auth", "otp", "reset-password"); private final WebClient webClient; // 构造注入替代字段注入,便于测试时替换依赖 @Autowired public AuthAccountFilter(WebClient.Builder webClientBuilder) { this.webClient = webClientBuilder.build(); } @Override public Class<Config> getConfigClass() { return Config.class; } public static class Config { // 空配置类,用于适配GatewayFilterFactory规范 } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { String endpoint = exchange.getRequest().getPath().toString().toLowerCase(); LOGGER.trace("处理端点: {}", endpoint); LOGGER.info("检查端点权限: {}", endpoint); // 判断是否为公共端点 boolean isPublic = PUBLIC_ENDPOINT_KEYWORDS.stream().anyMatch(endpoint::contains); if (isPublic) { LOGGER.info("公共端点,直接放行"); return chain.filter(exchange); } // 安全端点:校验JWT令牌 String jwtToken = exchange.getRequest().getHeaders().getFirst("Authorization"); if (jwtToken == null || !jwtToken.startsWith("Bearer ")) { LOGGER.warn("安全端点缺少有效JWT令牌"); return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "未提供有效认证令牌")); } // 调用JWT验证逻辑,验证通过则放行 return validateJwtToken(jwtToken.substring(7)) .flatMap(valid -> { if (valid) { LOGGER.info("JWT验证通过,放行请求"); return chain.filter(exchange); } else { return Mono.error(new ResponseStatusException(HttpStatus.FORBIDDEN, "认证令牌无效")); } }); }; } // 提取JWT验证方法,方便单独测试 private Mono<Boolean> validateJwtToken(String token) { try { JWSObject jwsObject = JWSObject.parse(token); JSONObject payload = jwsObject.getPayload().toJSONObject(); // 简单验证过期时间(实际场景可替换为调用认证服务校验) long exp = (long) payload.get("exp"); return Mono.just(exp > System.currentTimeMillis() / 1000); } catch (ParseException e) { LOGGER.error("解析JWT失败", e); return Mono.just(false); } } }
二、编写正确的单元测试
单元测试需覆盖公共端点放行、无JWT返回401、无效JWT返回403、有效JWT放行四个核心场景,使用WebFlux标准测试工具StepVerifier验证响应式流行为:
package gateway.filters; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.http.HttpHeaders; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class AuthAccountFilterTest { @Mock private GatewayFilterChain filterChain; @InjectMocks private AuthAccountFilter authAccountFilter; // 测试公共端点直接放行 @Test void testPublicEndpoint_ShouldPassThrough() { MockServerHttpRequest request = MockServerHttpRequest.get("/my-service/auth/login") .build(); MockServerWebExchange exchange = MockServerWebExchange.from(request); when(filterChain.filter(exchange)).thenReturn(Mono.empty()); AuthAccountFilter.Config config = new AuthAccountFilter.Config(); GatewayFilter filter = authAccountFilter.apply(config); Mono<Void> result = filter.filter(exchange, filterChain); StepVerifier.create(result) .verifyComplete(); verify(filterChain, times(1)).filter(exchange); } // 测试安全端点无JWT返回401 @Test void testSecureEndpoint_NoJwt_ShouldReturn401() { MockServerHttpRequest request = MockServerHttpRequest.get("/my-service/user/profile") .build(); MockServerWebExchange exchange = MockServerWebExchange.from(request); AuthAccountFilter.Config config = new AuthAccountFilter.Config(); GatewayFilter filter = authAccountFilter.apply(config); Mono<Void> result = filter.filter(exchange, filterChain); StepVerifier.create(result) .expectErrorMatches(throwable -> throwable instanceof org.springframework.web.server.ResponseStatusException && ((org.springframework.web.server.ResponseStatusException) throwable).getStatus().value() == 401) .verify(); verify(filterChain, never()).filter(exchange); } // 测试安全端点无效JWT返回403 @Test void testSecureEndpoint_InvalidJwt_ShouldReturn403() { MockServerHttpRequest request = MockServerHttpRequest.get("/my-service/user/profile") .header(HttpHeaders.AUTHORIZATION, "Bearer invalid-token") .build(); MockServerWebExchange exchange = MockServerWebExchange.from(request); AuthAccountFilter.Config config = new AuthAccountFilter.Config(); GatewayFilter filter = authAccountFilter.apply(config); Mono<Void> result = filter.filter(exchange, filterChain); StepVerifier.create(result) .expectErrorMatches(throwable -> throwable instanceof org.springframework.web.server.ResponseStatusException && ((org.springframework.web.server.ResponseStatusException) throwable).getStatus().value() == 403) .verify(); verify(filterChain, never()).filter(exchange); } // 测试安全端点有效JWT放行 @Test void testSecureEndpoint_ValidJwt_ShouldPassThrough() { // 生成过期时间为未来的有效JWT String validJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE5MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; MockServerHttpRequest request = MockServerHttpRequest.get("/my-service/user/profile") .header(HttpHeaders.AUTHORIZATION, "Bearer " + validJwt) .build(); MockServerWebExchange exchange = MockServerWebExchange.from(request); when(filterChain.filter(exchange)).thenReturn(Mono.empty()); AuthAccountFilter.Config config = new AuthAccountFilter.Config(); GatewayFilter filter = authAccountFilter.apply(config); Mono<Void> result = filter.filter(exchange, filterChain); StepVerifier.create(result) .verifyComplete(); verify(filterChain, times(1)).filter(exchange); } }
三、测试核心要点
- 使用
MockitoExtension适配JUnit 5,替代旧版SpringRunner - 用
StepVerifier验证响应式流的完成、错误状态,这是WebFlux测试的标准方式 - 每个测试用例仅验证单一场景,保持测试逻辑清晰
- 构造注入依赖,便于测试时替换模拟实现(如需验证WebClient调用,可替换为
WebClient的Mock实例)
内容的提问来源于stack exchange,提问作者Amine Ch 99
相关产品推荐
相关产品推荐

