1. 介绍

集成测试对验证应用正常运行至关重要,认证模块作为敏感部分必须严格测试。Testcontainers允许我们在测试阶段启动Docker容器,用真实技术栈运行测试。

本文将展示如何使用Testcontainers测试真实的Keycloak实例。

2. 搭建Spring Security与Keycloak集成

需要配置Spring Security、Keycloak和Testcontainers三部分。

2.1. 初始化Spring Boot和Spring Security

首先添加安全依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

创建一个返回用户信息的简单控制器:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("me")
    public UserDto getMe() {
        return new UserDto(1L, "janedoe", "Doe", "Jane", "jane.doe@example.com");
    }
}

现在我们有一个响应*/users/me的安全控制器。启动时Spring Security会生成用户'user'的密码(日志可见)。

2.2. 配置Keycloak

**最简单的方式是用Docker**:

docker run -p 8081:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:17.0.1 start-dev

访问http://localhost:8081进入控制台: Keycloak登录页

创建名为baeldung的realm: 创建Realm

添加名为baeldung-api的客户端: 创建客户端

通过用户菜单添加Jane Doe: 创建用户

设置密码s3cr3t并取消临时密码: 更新密码

现在已配置好包含baeldung-api客户端和Jane Doe用户的realm

2.3. 集成配置

添加OAuth2资源服务器依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

配置Spring Security:

@Configuration
@ConditionalOnProperty(name = "keycloak.enabled", havingValue = "true", matchIfMissing = true)
public class WebSecurityConfiguration {

    @Bean
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new NullAuthenticatedSessionStrategy();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        return http.csrf()
            .disable()
            .cors()
            .and()
            .authorizeHttpRequests(auth -> auth.anyRequest()
                .authenticated())
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
            .build();
    }
}

关键点:

  • 使用NullAuthenticatedSessionStrategy实现无状态认证
  • @ConditionalOnProperty支持通过keycloak.enabled=false禁用Keycloak

application.properties添加连接配置:

keycloak.enabled=true
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/auth/realms/baeldung-api

现在应用已受保护,每次请求都会向Keycloak验证认证信息

3. 使用Testcontainers配置Keycloak

3.1. 导出Realm配置

Keycloak容器启动时无配置,需通过JSON文件导入。从运行实例导出配置: 导出配置

注意:管理界面不导出用户,需手动编辑realm-export.json添加用户:

"users": [
  {
    "username": "janedoe",
    "email": "jane.doe@example.com",
    "firstName": "Jane",
    "lastName": "Doe",
    "enabled": true,
    "credentials": [
      {
        "type": "password",
        "value": "s3cr3t"
      }
    ],
    "clientRoles": {
      "account": [
        "view-profile",
        "manage-account"
      ]
    }
  }
]

将文件放入src/test/resources/keycloak目录。

3.2. 配置Testcontainers

添加依赖:

<dependency>
    <groupId>com.github.dasniko</groupId>
    <artifactId>testcontainers-keycloak</artifactId>
    <version>2.1.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.16.3</version>
</dependency>

创建基础测试类:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public abstract class KeycloakTestContainers {

    static {
        keycloak = new KeycloakContainer().withRealmImportFile("keycloak/realm-export.json");
        keycloak.start();
    }
}

静态初始化确保容器只启动一次withRealmImportFile指定启动时导入的realm配置。

3.3. Spring Boot测试配置

Keycloak使用随机端口,需动态覆盖配置:

@DynamicPropertySource
static void registerResourceServerIssuerProperty(DynamicPropertyRegistry registry) {
    registry.add("spring.security.oauth2.resourceserver.jwt.issuer-uri", 
        () -> keycloak.getAuthServerUrl() + "/realms/baeldung");
}

4. 创建集成测试

4.1. 获取访问令牌

在抽象类添加获取令牌的方法:

URI authorizationURI = new URIBuilder(keycloak.getAuthServerUrl() + "/realms/baeldung/protocol/openid-connect/token").build();
WebClient webclient = WebClient.builder().build();
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.put("grant_type", Collections.singletonList("password"));
formData.put("client_id", Collections.singletonList("baeldung-api"));
formData.put("username", Collections.singletonList("jane.doe@example.com"));
formData.put("password", Collections.singletonList("s3cr3t"));

String result = webclient.post()
  .uri(authorizationURI)
  .contentType(MediaType.APPLICATION_FORM_URLENCODED)
  .body(BodyInserters.fromFormData(formData))
  .retrieve()
  .bodyToMono(String.class)
  .block();

解析响应提取令牌:

JacksonJsonParser jsonParser = new JacksonJsonParser();
return "Bearer " + jsonParser.parseMap(result)
  .get("access_token")
  .toString();

4.2. 创建集成测试

添加RestAssured依赖:

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
</dependency>

编写测试用例:

@Test
void givenAuthenticatedUser_whenGetMe_shouldReturnMyInfo() {

    given().header("Authorization", getJaneDoeBearer())
      .when()
      .get("/users/me")
      .then()
      .body("username", equalTo("janedoe"))
      .body("lastname", equalTo("Doe"))
      .body("firstname", equalTo("Jane"))
      .body("email", equalTo("jane.doe@example.com"));
}

测试将Keycloak获取的访问令牌添加到请求头

5. 总结

本文展示了如何使用Testcontainers测试真实的Keycloak实例,通过导入realm配置确保测试环境一致性。完整代码示例见GitHub


原始标题:Spring Boot – Keycloak Integration Testing with Testcontainers