Spring Boot 3 + Keycloak Realm Role 설정

Table of Contents

한 줄 요약

Spring Boot 3에서는 Keycloak의 realm_access.roles를 기본값만으로는 권한으로 쓰기 어렵습니다. JwtAuthenticationConverter에 커스텀 converter를 연결해 ROLE_*로 명시 매핑해야 합니다.

왜 자주 403이 나는가

다음 두 조건이 동시에 만족되어야 hasRole("admin")가 동작합니다.

  1. Access token에 realm_access.roles가 실제로 존재
  2. Spring Security의 GrantedAuthority로 ROLE_admin이 생성

Keycloak에서 role을 발급해도 2번이 빠지면 인증은 성공(200/401)해도 인가에서 403이 발생합니다.

토큰에서 먼저 볼 필드

 1{
 2  "iss": "http://localhost:8080/realms/my-realm",
 3  "realm_access": {
 4    "roles": ["admin", "user"]
 5  },
 6  "resource_access": {
 7    "my-api": {
 8      "roles": ["api.read"]
 9    }
10  },
11  "scope": "profile email"
12}
  • iss: issuer-uri와 정확히 일치해야 함
  • realm_access.roles: realm role 매핑 대상
  • resource_access.{client}.roles: client role이 필요할 때만 별도 매핑

권장 구현

핵심은 SCOPE_* 권한은 유지하고, realm role을 추가로 합치는 방식입니다.

 1@Configuration
 2@EnableMethodSecurity
 3public class SecurityConfig {
 4
 5    @Bean
 6    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 7        http
 8            .csrf(AbstractHttpConfigurer::disable)
 9            .authorizeHttpRequests(auth -> auth
10                .requestMatchers("/actuator/health").permitAll()
11                .requestMatchers("/admin/**").hasRole("admin")
12                .anyRequest().authenticated())
13            .oauth2ResourceServer(oauth2 -> oauth2
14                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));
15        return http.build();
16    }
17
18    @Bean
19    Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
20        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
21        converter.setJwtGrantedAuthoritiesConverter(new KeycloakJwtAuthoritiesConverter());
22        return converter;
23    }
24}
 1public class KeycloakJwtAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
 2
 3    private final JwtGrantedAuthoritiesConverter defaultScopes = new JwtGrantedAuthoritiesConverter();
 4
 5    @Override
 6    public Collection<GrantedAuthority> convert(Jwt jwt) {
 7        Set<GrantedAuthority> authorities = new HashSet<>(defaultScopes.convert(jwt));
 8
 9        Map<String, Object> realmAccess = jwt.getClaim("realm_access");
10        if (realmAccess == null) {
11            return authorities;
12        }
13
14        Object rolesObj = realmAccess.get("roles");
15        if (!(rolesObj instanceof Collection<?> roles)) {
16            return authorities;
17        }
18
19        roles.stream()
20            .filter(String.class::isInstance)
21            .map(String.class::cast)
22            .map(role -> "ROLE_" + role)
23            .map(SimpleGrantedAuthority::new)
24            .forEach(authorities::add);
25
26        return authorities;
27    }
28}

application.yml

1spring:
2  security:
3    oauth2:
4      resourceserver:
5        jwt:
6          issuer-uri: http://localhost:8080/realms/my-realm

실제 문제 사례

사례 1: hasRole("admin")만 403

  • 원인: 커스텀 converter 미적용
  • 해결: realm_access.roles -> ROLE_* 매핑 추가

사례 2: 운영에서만 401

  • 원인: 토큰 iss와 서비스 issuer-uri 불일치
  • 해결: 운영 토큰 기준으로 issuer-uri 교정

사례 3: scope 기반 API가 갑자기 막힘

  • 원인: 커스텀 converter가 default scope 권한을 덮어씀
  • 해결: 기본 JwtGrantedAuthoritiesConverter 결과를 merge

점검 체크리스트

  1. 토큰에 realm_access.roles가 있는가
  2. ROLE_ prefix로 authority가 생성되는가
  3. @EnableMethodSecurity가 활성화되어 있는가
  4. issuer-uri와 iss가 일치하는가
  5. 기존 SCOPE_* 권한이 유지되는가

정리

Spring Boot 3 + Keycloak 권한 이슈는 대부분 "토큰 미발급"보다 "권한 매핑 누락"에서 발생합니다. 인증(401)과 인가(403)를 분리해 확인하면 장애 대응 속도가 빨라집니다.

댓글