You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 17:47:02