Keycloak

[Keycloak] Spring Boot Resource Server 연동하기

kyoulho 2025. 3. 26. 19:55
728x90

이 글에서는 Keycloak 26과 Spring Boot 기반의 리소스 서버 간의 JWT 연동 방법을 설명합니다. 특히, 리소스 서버가 Keycloak에 클라이언트로 별도 등록되지 않고도 JWT를 검증하는 방법, JWT 파싱 컨버터 설정 및 기본 보안 설정을 다룹니다.

  • 리소스 서버는 Keycloak에 클라이언트로 등록될 필요가 없습니다.
  • JWT의 iss 클레임과 리소스 서버의 issuer-uri 설정이 정확히 일치해야 합니다.
  • 리소스 서버는 {issuer-uri}/protocol/openid-connect/certs를 통해 
  • JWT 파싱을 위한 컨버터 설정이 필요합니다.
  • 간단한 Security 설정을 진행합니다.


Spring Boot 프로젝트 의존성 설정

Gradle 설정 예시 (build.gradle.kts)

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
    implementation("org.springframework.boot:spring-boot-starter-security")
}

application.yml 설정

JWT 발급자(issuer)를 검증하기 위해 Keycloak이 발급하는 정확한 iss를 확인하고 설정합니다.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://{keycloak-hostname}/{relative-path}/realms/{your-realm}
          # jwk-set-uri는 생략 가능합니다. 필요할 경우 아래처럼 명시적으로 설정 가능합니다.
          # jwk-set-uri: https://{keycloak-hostname}/{relative-path}/realms/{your-realm}/protocol/openid-connect/certs

 

Spring Security는 issuer-uri만 설정해도 자동으로 OpenID Connect Discovery Endpoint를 통해 jwk-set-uri를 찾습니다. 다만, 특정 환경이나 네트워크 문제 발생 시 명시적으로 jwk-set-uri를 설정하는 것이 좋습니다.

리소스 서버가 certs 엔드포인트에서 수행하는 작업

리소스 서버는 {issuer-uri}/protocol/openid-connect/certs에서 JWT 서명을 검증하기 위한 공개 키(JWK)를 가져옵니다.

  • JWT는 Keycloak의 개인 키로 서명됩니다.
  • 리소스 서버는 Keycloak이 제공하는 공개 키를 사용하여 JWT 서명의 유효성을 검증합니다.
  • 이를 통해 JWT가 신뢰할 수 있는 소스로부터 발급되었는지 확인합니다.

Keycloak JWT 발급자(issuer, iss) 확인 방법

Keycloak이 JWT의 iss 클레임을 결정할 때 사용하는 환경 변수는 다음과 같습니다.

# deployment.yml

- name: KEYCLOAK_HTTP_RELATIVE_PATH
  value: "/keycloak"
- name: KEYCLOAK_HOSTNAME
  value: "auth.example.com/keycloak"
- name: KEYCLOAK_PROXY
  value: "edge"
- name: KEYCLOAK_PROXY_HEADERS
  value: "x-forwarded"

위와 같은 환경변수를 설정한 경우, Keycloak이 발급하는 JWT의 iss 클레임 값은 다음과 같습니다.

https://auth.example.com/keycloak/realms/{realm-name}

리소스 서버는 이 값을 반드시 일치하도록 설정해야 정상적으로 JWT를 검증할 수 있습니다.


JwtAuthenticationConverter 설정

JWT에서 사용자 정보와 권한을 추출하는 컨버터를 설정합니다.

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import java.util.List;
import java.util.UUID;

public class KeyclockJwtConverter implements Converter<Jwt, AbstractAuthenticationToken> {

    @Override
    public AbstractAuthenticationToken convert(@NotNull Jwt jwt) {
        List<GrantedAuthority> authorities = findAuthorities(jwt);

        return new JwtAuthenticationToken(jwt, authorities, jwt.getSubject());
    }

    private List<GrantedAuthority> findAuthorities(Jwt jwt) {
       ... 생략
    }
}


SecurityConfig 설정

JWT 검증을 위한 간단한 보안 설정을 구성합니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
@Configuration
public class OAuth2SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().permitAll()
                );
        http
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwtConfigurer ->
                        jwtConfigurer.jwtAuthenticationConverter(new KeyclockJwtConverter())
                ));

        http
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}
728x90