Spring Boot 3 + Keycloak Realm Role 설정
Table of Contents
한 줄 요약
Spring Boot 3에서는 Keycloak의 realm_access.roles를 기본값만으로는 권한으로 쓰기 어렵습니다. JwtAuthenticationConverter에 커스텀 converter를 연결해 ROLE_*로 명시 매핑해야 합니다.
왜 자주 403이 나는가
다음 두 조건이 동시에 만족되어야 hasRole("admin")가 동작합니다.
- Access token에
realm_access.roles가 실제로 존재 - 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
점검 체크리스트
- 토큰에
realm_access.roles가 있는가 ROLE_prefix로 authority가 생성되는가@EnableMethodSecurity가 활성화되어 있는가issuer-uri와iss가 일치하는가- 기존
SCOPE_*권한이 유지되는가
정리
Spring Boot 3 + Keycloak 권한 이슈는 대부분 "토큰 미발급"보다 "권한 매핑 누락"에서 발생합니다. 인증(401)과 인가(403)를 분리해 확인하면 장애 대응 속도가 빨라집니다.