1. 소개

통합 테스트는 애플리케이션이 제대로 작동하는지 검증할 때 중요합니다. 또한 인증은 민감한 부분이므로 정확하게 테스트해야 합니다 . Testcontainers를 사용하면 테스트 단계에서 Docker 컨테이너를 시작하여 실제 기술 스택에 대한 테스트를 실행할 수 있습니다.

이 기사에서는 Testcontainers를 사용하여 실제 Keycloak 인스턴스 에 대한 통합 테스트를 설정하는 방법을 살펴 봅니다.

2. Keycloak으로 스프링 Security 설정

Spring Security , Keycloak 구성 및 마지막으로 Testcontainers 를 설정해야 합니다 .

2.1. 스프링 부트 및 스프링 Security 설정

Spring Security 덕분에 Security 설정부터 시작하겠습니다. spring-boot-starter-security 의존성 이 필요합니다 . 이제 pom에 추가해 보겠습니다.

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

우리는 spring-boot 부모 pom을 사용할 것입니다. 따라서 의존성 관리에 지정된 라이브러리 버전을 지정할 필요가 없습니다.

다음으로 사용자를 반환하는 간단한 컨트롤러를 만들어 보겠습니다.

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

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

이 시점 에서 " /users/me" 에 대한 요청에 응답하는 Security 컨트롤러가 있습니다. 애플리케이션을 시작할 때 Spring Security는 애플리케이션 로그에 표시되는 사용자 'user'의 비밀번호를 생성합니다.

2.2. Keycloak 구성

로컬 Keycloak을 실행하는 가장 쉬운 방법은 Docker 를 사용하는 것 입니다. 따라서 이미 구성된 관리자 계정으로 Keycloak 컨테이너를 실행해 보겠습니다.

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

URL http://localhost:8081 에 대한 브라우저를 열어서 Keycloak 콘솔에 액세스해 보겠습니다.

Keycloak 로그인 페이지

다음으로 영역을 만들어 보겠습니다. 우리는 그것을 baeldung이라고 부를 것입니다:

Keycloak 생성 영역

baeldung-api라는 클라이언트를 추가해야 합니다.

Keycloak 생성 클라이언트

마지막으로 사용자 메뉴를 사용하여 Jane Doe 사용자를 추가해 보겠습니다.

Keycloak 사용자 생성

이제 사용자를 생성했으므로 암호를 할당해야 합니다. s3cr3t를 선택하고 임시 버튼을 선택 취소합니다.

Keycloak 업데이트 비밀번호

이제 baeldung-api 클라이언트와 Jane Doe 사용자로 Keycloak 영역을 설정했습니다 .

다음으로 Keycloak을 ID Provider로 사용하도록 Spring을 구성합니다.

2.3. 둘 다 합치기

먼저 식별 제어를 Keycloak 서버에 Delegation합니다. 이를 위해 spring-boot-starter-oauth2-resource-server 라이브러리를 사용합니다. Keycloak 서버에서 JWT 토큰의 유효성을 검사할 수 있습니다. 따라서 pom에 추가해 보겠습니다.

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

OAuth 2 리소스 서버 지원 을 추가하도록 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();
    }
}

들어오는 모든 요청에 ​​적용할 새 필터 체인을 설정하고 있습니다. Keycloak 서버에 대해 바인딩된 JWT 토큰의 유효성을 검사합니다.

베어러 전용 인증으로 상태 비저장 애플리케이션을 구축할  때 NullAuthenticatedSessionStrategy 를 세션 전략으로 사용합니다 . 또한 @ConditionalOnProperty 를 사용하면 keycloak.enabled 속성을 false 로 설정하여 Keycloak 구성 을 비활성화할 수 있습니다 .

마지막으로 application.properties 파일 에서 Keycloak에 연결하는 데 필요한 구성을 추가해 보겠습니다 .

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

우리의 응용 프로그램은 이제 안전하며 인증을 확인하기 위해 각 요청에 대해 Keycloak을 쿼리합니다 .

3. Keycloak용 테스트 컨테이너 설정

3.1. 영역 구성 내보내기

Keycloak 컨테이너는 구성 없이 시작됩니다. 따라서 컨테이너가 JSON 파일로 시작될 때 가져와야 합니다 . 현재 실행 중인 인스턴스에서 이 파일을 내보내겠습니다.

2 2022-06-22-22-56-31-1 스크린샷

안타깝게도 Keycloak은 관리 인터페이스를 통해 사용자를 내보내지 않습니다. 컨테이너에 로그인하고 kc.sh 내보내기 명령을 사용할 수 있습니다. 이 예에서는 결과 realm-export.json 파일을 수동으로 편집하고 여기에 Jane Doe를 추가하는 것이 더 쉽습니다. 마지막 중괄호 바로 앞에 이 구성을 추가해 보겠습니다.

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

src/test/resources/keycloak 폴더 의 프로젝트에 realm-export.json 파일을 포함시키겠습니다 . Keycloak 컨테이너를 출시하는 동안 사용할 것입니다.

3.2. 테스트 컨테이너 설정

testcontainers 의존성과 testcontainers-keycloak 를 추가하여 Keycloak 컨테이너를 시작할 수 있도록 합시다.

<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>

다음으로 모든 테스트가 파생될 클래스를 만들어 보겠습니다. 이를 사용하여 Testcontainers에서 시작한 Keycloak 컨테이너를 구성합니다.

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

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

컨테이너를 정적으로 선언하고 시작하면 모든 테스트에 대해 컨테이너가 한 번 인스턴스화되고 시작됩니다. KeycloakContainer 개체 에서 withRealmImportFile 메서드 를 사용하여 시작할 때 가져올 영역의 구성을 지정하고 있습니다.

3.3. 스프링 부트 테스트 구성

Keycloak 컨테이너는 랜덤의 포트를 사용합니다. 따라서 일단 시작되면 application.properties  에 정의된 spring.security.oauth2.resourceserver.jwt.issuer-uri 구성을 재정의해야 합니다. 이를 위해 편리한 @DynamicPropertySource 어노테이션을 사용합니다.

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

4. 통합 테스트 만들기

이제 Keycloak 컨테이너를 시작하고 Spring 속성을 구성하는 주요 테스트 클래스가 있으므로 사용자 컨트롤러 를 호출 하는 통합 테스트 를 생성해 보겠습니다.

4.1. 액세스 토큰 얻기

먼저 추상 클래스 IntegrationTest에 Jane Doe의 자격 증명으로 토큰을 요청하는 메서드를 추가해 보겠습니다.

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@baeldung.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();

여기에서는 Webflux의 WebClient를 사용하여 액세스 토큰을 얻는 데 필요한 다양한 매개 변수가 포함된 양식을 게시합니다.

마지막으로 Keycloak 서버 응답을 구문 분석하여 토큰을 추출합니다 . 특히 Bearer 키워드와 토큰 콘텐츠가 포함된 클래식 인증 문자열을 생성하여 헤더에 사용할 준비가 되었습니다.

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

4.2. 통합 테스트 만들기

구성된 Keycloak 컨테이너에 대한 통합 테스트를 빠르게 설정해 보겠습니다. 우리는 테스트를 위해 RestAssured와 Hamcrest를 사용할 것입니다. 안심 의존성 을 추가해 보겠습니다 .

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

이제 추상 IntegrationTest 클래스를 사용하여 테스트를 만들 수 있습니다.

@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@baeldung.com"));
}

결과적으로 Keycloak에서 가져온 액세스 토큰이 요청의 Authorization 헤더에 추가됩니다.

5. 결론

이 기사에서는 Testcontainers에서 관리하는 실제 Keycloak에 대한 통합 테스트를 설정했습니다 . 테스트 단계를 시작할 때마다 미리 구성된 환경을 갖도록 영역 구성을 가져왔습니다.

늘 그렇듯이 이 기사에 사용된 모든 코드 샘플 은 GitHub 에서 찾을 수 있습니다 .

Security footer banner