如何在Servlet型Spring Boot测试中配置WebTestClient适配OAuth2.0登录
两种方案解决Servlet型Spring Boot应用WebTestClient的OAuth2登录测试
你在Servlet型Spring Boot应用中使用WebTestClient编写端到端测试,添加Spring Security OAuth2登录后需要适配测试逻辑。下面提供两种可行方案:
一、使用Keycloak测试容器实现真实登录
这种方案模拟真实的认证流程,适合真正的端到端测试场景:
1. 添加依赖
在pom.xml(Maven)中添加测试容器和Keycloak管理客户端依赖:
<dependency> <groupId>org.testcontainers</groupId> <artifactId>keycloak</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-admin-client</artifactId> <scope>test</scope> </dependency>
2. 配置Keycloak测试容器
创建测试配置类,启动Keycloak容器并初始化测试用的Realm、客户端和用户:
@TestConfiguration public class KeycloakTestContainerConfig { private static final KeycloakContainer KEYCLOAK_CONTAINER = new KeycloakContainer("quay.io/keycloak/keycloak:22.0"); static { KEYCLOAK_CONTAINER.start(); // 初始化测试数据 Keycloak adminClient = KeycloakBuilder.builder() .serverUrl(KEYCLOAK_CONTAINER.getAuthServerUrl()) .realm("master") .username(KEYCLOAK_CONTAINER.getAdminUsername()) .password(KEYCLOAK_CONTAINER.getAdminPassword()) .clientId("admin-cli") .build(); // 创建测试Realm RealmRepresentation testRealm = new RealmRepresentation(); testRealm.setRealm("test-realm"); testRealm.setEnabled(true); adminClient.realms().create(testRealm); // 创建允许密码模式的客户端 ClientRepresentation testClient = new ClientRepresentation(); testClient.setClientId("test-client"); testClient.setDirectAccessGrantsEnabled(true); testClient.setPublicClient(true); adminClient.realm("test-realm").clients().create(testClient); // 创建测试用户 UserRepresentation testUser = new UserRepresentation(); testUser.setUsername("test-user"); testUser.setPassword("test-pass"); testUser.setEnabled(true); adminClient.realm("test-realm").users().create(testUser); } @Bean public KeycloakContainer keycloakContainer() { return KEYCLOAK_CONTAINER; } }
3. 在测试类中获取令牌并请求
修改测试类,获取Keycloak颁发的访问令牌,携带令牌发起请求:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") @Import(KeycloakTestContainerConfig.class) public class End2EndTest { @Autowired protected WebTestClient webClient; @Autowired private KeycloakContainer keycloakContainer; private String getAccessToken() { MultiValueMap<String, String> formData = new LinkedMultiValueMap<>(); formData.put("grant_type", Collections.singletonList("password")); formData.put("client_id", Collections.singletonList("test-client")); formData.put("username", Collections.singletonList("test-user")); formData.put("password", Collections.singletonList("test-pass")); ResponseEntity<Map> tokenResponse = new RestTemplate().postForEntity( keycloakContainer.getAuthServerUrl() + "/realms/test-realm/protocol/openid-connect/token", formData, Map.class ); return (String) tokenResponse.getBody().get("access_token"); } @Test public void testAuthenticatedTaskRequest() { String token = getAccessToken(); webClient .get() .uri("/api/v1/tasks") .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) .exchange() .expectStatus().isOk(); } }
二、Mock OAuth2登录(无需真实认证服务器)
如果不需要真实认证流程,可通过以下方式模拟登录:
方式1:生成模拟JWT令牌(适配真实服务器模式)
在测试环境配置宽松的JWT验证逻辑,生成模拟令牌携带请求:
测试环境Security配置
@Profile("test") @Configuration public class TestSecurityConfig { @Bean SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(c -> c .requestMatchers(GET, "/v1/api-docs/**").permitAll() .requestMatchers("/api/v1/**").authenticated() .anyRequest().denyAll()) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); return http.build(); } @Bean JwtDecoder jwtDecoder() { // 生成RSA密钥对 KeyPair keyPair = generateRsaKeyPair(); // 创建宽松的解码器,接受自定义签发的令牌 NimbusJwtDecoder decoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build(); decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer("test-issuer")); return decoder; } @Bean KeyPair rsaKeyPair() { return generateRsaKeyPair(); } private KeyPair generateRsaKeyPair() { try { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(2048); return generator.generateKeyPair(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Failed to generate RSA key pair", e); } } }
测试类中生成模拟令牌
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") public class End2EndTest { @Autowired protected WebTestClient webClient; @Autowired private KeyPair rsaKeyPair; private String generateMockJwt() { JwtClaimsSet claims = JwtClaimsSet.builder() .issuer("test-issuer") .subject("test-user") .claim("roles", Collections.singletonList("USER")) .expiresAt(new Date(System.currentTimeMillis() + 3600000)) .build(); return Jwts.builder() .setClaims(claims) .signWith(rsaKeyPair.getPrivate(), SignatureAlgorithm.RS256) .compact(); } @Test public void testMockAuthenticatedRequest() { String token = generateMockJwt(); webClient .get() .uri("/api/v1/tasks") .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) .exchange() .expectStatus().isOk(); } }
方式2:结合MockMvc使用mockOidcLogin(仅Mock服务器模式)
如果将测试改为使用WebEnvironment.MOCK(启动MockWebServer),可直接使用Spring Security的mockOidcLogin处理器:
@SpringBootTest(webEnvironment = WebEnvironment.MOCK) @ActiveProfiles("test") @AutoConfigureMockMvc public class End2EndTest { @Autowired private MockMvc mockMvc; private WebTestClient webClient; @BeforeEach void setupWebClient() { webClient = WebTestClient.bindToMockMvc(mockMvc) .apply(SecurityMockServerConfigurers.springSecurity()) .build(); } @Test public void testWithMockOidcLogin() { webClient .mutateWith(mockOidcLogin().user(user -> user.username("test-user"))) .get() .uri("/api/v1/tasks") .exchange() .expectStatus().isOk(); } }
注意:此方式为Mock服务器模式,不属于真实端到端测试,适合集成测试场景。
内容的提问来源于stack exchange,提问作者Ivan
相关产品推荐
相关产品推荐

