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 콘솔에 액세스해 보겠습니다.
다음으로 영역을 만들어 보겠습니다. 우리는 그것을 baeldung이라고 부를 것입니다:
baeldung-api라는 클라이언트를 추가해야 합니다.
마지막으로 사용자 메뉴를 사용하여 Jane Doe 사용자를 추가해 보겠습니다.
이제 사용자를 생성했으므로 암호를 할당해야 합니다. s3cr3t를 선택하고 임시 버튼을 선택 취소합니다.
이제 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 파일로 시작될 때 가져와야 합니다 . 현재 실행 중인 인스턴스에서 이 파일을 내보내겠습니다.
안타깝게도 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 에서 찾을 수 있습니다 .