92 lines
11 KiB
Markdown
92 lines
11 KiB
Markdown
---
|
|
title: "Extract roles from access token issued by Keycloak using Spring Security (Between Data / Christian Huff)"
|
|
source_type: personal-blog
|
|
url: https://betweendata.io/posts/secure-spring-rest-api-using-keycloak/
|
|
archive_url:
|
|
related_branches: [feature-keycloak-spring-rs-role-mapping]
|
|
related_projects: [keycloak-patterns]
|
|
tags: [personal-blog, keycloak-patterns, auth, spring-security, keycloak]
|
|
status: raw
|
|
confidence: medium
|
|
created: 2026-07-18
|
|
last_reviewed: 2026-07-18
|
|
---
|
|
|
|
# Extract roles from access token issued by Keycloak using Spring Security (Between Data / Christian Huff)
|
|
|
|
> Layer: `raw/company-tech-blogs/` (분류: 실제 `source_type` 은 `personal-blog` — 저자 Christian Huff 개인 블로그. 저장소 기존 관행([[raw/company-tech-blogs/test-pyramid-vs-trophy-kent-dodds]], `senior-engineer-competency-mubin-shaikh.md`, `deliberate-practice-software-developers-redgreencode.md`)에 따라 `company-tech-blogs/` 디렉토리에 위치하되 frontmatter `source_type: personal-blog` 유지).
|
|
> 회사 기술 블로그가 아니므로 **공식 best practice 로 격상 금지** (CLAUDE.md §5, §11). 아래 모든 Claim 은 `engineering-blog` 강도 — 개인 저자의 구현 사례일 뿐, Spring/Keycloak 공식 권고가 아니다.
|
|
|
|
## Parent / 활용 branch (필수)
|
|
|
|
| Branch | 이 자료가 정당화하는 결정 |
|
|
|---|---|
|
|
| [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] | Keycloak realm role 을 Spring `GrantedAuthority` 로 매핑하는 구현 방식 — 손으로 작성한 `Converter<Jwt, Collection<GrantedAuthority>>` 가 nested `realm_access` claim 을 읽어 `ROLE_` prefix 붙은 authority 로 변환하고, 필요 시 `DelegatingJwtGrantedAuthoritiesConverter` 로 default scope converter 와 결합하는 접근의 **사례(case study) 근거**. `feature-keycloak-spring-rs-role-mapping` D4 (`realm role 만 매핑`) 의 구현 detail 참고 자료.
|
|
|
|
## 출처 / Source
|
|
|
|
- 원본 URL: https://betweendata.io/posts/secure-spring-rest-api-using-keycloak/
|
|
- 아카이브 URL: (미수집)
|
|
- 저자 / 조직: Christian Huff (개인 블로그 "Between Data")
|
|
- 발행일: 2023-02-23
|
|
- 마지막 확인일: 2026-07-18
|
|
|
|
## 왜 저장했는지 / Why archived
|
|
|
|
Spring Boot 3 + `spring-boot-starter-oauth2-resource-server` 환경에서 Keycloak 이 발급한 JWT 의 `realm_access`/`resource_access` claim 은 Spring 의 기본 `JwtGrantedAuthoritiesConverter` 가 자동으로 추출하지 못한다. 이 자료는 그 문제를 **커스텀 `Converter<Jwt, Collection<GrantedAuthority>>`** 로 해결한 구체 코드 사례를 담고 있어, `feature-keycloak-spring-rs-role-mapping` 의 role mapping 구현 detail 을 정당화하는 참고 사례로 보관.
|
|
|
|
## 핵심 인용 / Key quotes (verbatim)
|
|
|
|
> [§Extract Roles from Access Token — class declaration] "public class KeycloakJwtRolesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {"
|
|
|
|
> [§Extract Roles from Access Token — realm_access claim 이름 정의 + 실제 읽기] "private static final String CLAIM_REALM_ACCESS = "realm_access";" [...] "Map<String, Collection<String>> realmAccess = jwt.getClaim(CLAIM_REALM_ACCESS);"
|
|
|
|
> [§Extract Roles from Access Token — ROLE_ prefix 상수] "public static final String PREFIX_REALM_ROLE = "ROLE_realm_";" [...] "public static final String PREFIX_RESOURCE_ROLE = "ROLE_";"
|
|
|
|
> [§Extract Roles from Access Token — ROLE_ prefix 설명 (본문)] "In the returned authorities the realm roles are prefixed with ROLE_realm_ while the resource roles are prefixed with ROLE_[NAME_OF_THE_RESOURCE]_."
|
|
|
|
> [§Define Access Rules — DelegatingJwtGrantedAuthoritiesConverter 조합] "new DelegatingJwtGrantedAuthoritiesConverter(" [...] "new JwtGrantedAuthoritiesConverter()," [...] "new KeycloakJwtRolesConverter());"
|
|
|
|
## Claims Extracted / 추출된 주장
|
|
|
|
| Claim ID | Claim (이 자료가 직접 말하는 것) | Evidence quote | Strength | Applies to | Does not prove |
|
|
|---|---|---|---|---|---|
|
|
| KC-ROLE-BD-C1 | 저자는 `Converter<Jwt, Collection<GrantedAuthority>>` 를 구현하는 `KeycloakJwtRolesConverter` 클래스를 작성해, `realm_access` claim 이름을 상수로 정의하고 `jwt.getClaim(CLAIM_REALM_ACCESS)` 로 직접 읽는다 | "public class KeycloakJwtRolesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {" / "private static final String CLAIM_REALM_ACCESS = \"realm_access\";" / "Map<String, Collection<String>> realmAccess = jwt.getClaim(CLAIM_REALM_ACCESS);" | `engineering-blog` | Spring Boot 3 + Spring Security OAuth2 Resource Server 환경에서 Keycloak `realm_access` (nested claim map) 을 `GrantedAuthority` 로 변환하는 구현 패턴 | 이 방식이 Spring 또는 Keycloak 공식 권고 패턴이라는 것은 아님 (원문에 공식 문서 인용 없음). Keycloak 모든 버전에서 `realm_access` claim 구조가 동일하다는 보증도 아님 — 원문 예시 토큰은 특정 시점(2023-02) Keycloak 버전 기준 |
|
|
| KC-ROLE-BD-C2 | realm-level role 은 `ROLE_realm_` prefix, resource(client)-level role 은 `ROLE_[리소스명]_` prefix 를 붙여 `SimpleGrantedAuthority` 로 변환한다고 명시 | "public static final String PREFIX_REALM_ROLE = \"ROLE_realm_\";" / "public static final String PREFIX_RESOURCE_ROLE = \"ROLE_\";" / "In the returned authorities the realm roles are prefixed with ROLE_realm_ while the resource roles are prefixed with ROLE_[NAME_OF_THE_RESOURCE]_." | `engineering-blog` | Spring Security `hasAuthority(...)` 매칭을 위한 authority 명명 규칙의 한 예시(개인 저자 관례) | `ROLE_` prefix 가 Spring Security 의 필수 요구사항이라는 것은 아님 — `hasAuthority` 는 임의 문자열 매칭이 가능하고, `ROLE_` prefix 규칙은 `hasRole(...)` 사용 시에만 Spring 이 자동으로 붙이는 것과는 다른 맥락(원문은 이 구분을 설명하지 않음) |
|
|
| KC-ROLE-BD-C3 | `WebSecurityConfiguration.filterChain(...)` 에서 `DelegatingJwtGrantedAuthoritiesConverter` 를 사용해 default `JwtGrantedAuthoritiesConverter` 와 커스텀 `KeycloakJwtRolesConverter` 를 함께 등록한다 | "new DelegatingJwtGrantedAuthoritiesConverter(" [...] "new JwtGrantedAuthoritiesConverter()," [...] "new KeycloakJwtRolesConverter());" | `engineering-blog` | scope 기반 default authority 와 realm/resource role 기반 custom authority를 하나의 authorities 집합으로 합치는 조합 패턴의 사례 | 이 조합이 모든 프로젝트에 필요하다는 것은 아님 — scope 기반 인가를 병행하지 않는 프로젝트라면 default converter 생략 가능. 원문도 코드 주석 수준("Using the delegating converter multiple converters can be combined")의 설명만 제공하며 `DelegatingJwtGrantedAuthoritiesConverter` API 계약 자체의 공식 문서화는 아님 |
|
|
|
|
### 참고: 이 raw 는 `engineering-blog` 강도만 제공 — official 보강 필요
|
|
|
|
`KC-ROLE-BD-C1`~`C3` 는 모두 `engineering-blog` (개인 저자 사례). branch-note 에서 이를 "공식 best practice" 로 인용하면 안 된다 (CLAUDE.md §5, §11). `realm_access` 가 default `JwtGrantedAuthoritiesConverter` 로 자동 매핑되지 않는다는 사실 자체의 공식 근거가 필요하면 [[raw/official-docs/spring-security-resource-server-jwt]] (예: 기존 branch-note 인용 `SSRS-JWT-C4` — default converter 는 `scope`/`scp` 만 `SCOPE_` prefix 로 자동 변환) 를 함께 인용해야 `official-vendor-doc` 급 근거가 된다. 이 raw 단독으로는 D4(realm role 만 매핑) 의 "왜 커스텀 컨버터가 필요한가"에 대한 **사례**일 뿐, "Spring 이 이렇게 하라고 권고한다"는 근거는 아니다.
|
|
|
|
## Usage Boundaries / 적용 경계
|
|
|
|
- **이 자료가 직접 증명하는 것**:
|
|
- `KC-ROLE-BD-C1`: 손으로 작성한 `Converter<Jwt, Collection<GrantedAuthority>>` 구현이 `realm_access` claim 을 nested map 으로 읽어올 수 있다는 동작 사례 (저자 GitHub 리포지토리에 테스트 100% 커버리지 존재한다고 원문이 주장 — 코드 자체는 미검증)
|
|
- `KC-ROLE-BD-C2`: `ROLE_realm_` / `ROLE_[resource]_` prefix 부여 방식 예시
|
|
- `KC-ROLE-BD-C3`: `DelegatingJwtGrantedAuthoritiesConverter` 로 default + custom converter 를 합치는 코드 구조 예시
|
|
- **이 자료가 증명하지 않는 것**:
|
|
- 이 구현이 Spring Security 또는 Keycloak 의 공식 권장 패턴이라는 명제 — 원문은 개인 저자의 "minimally invasive" 선택 설명일 뿐, RFC/공식 문서 인용 없음
|
|
- `realm_access.roles` 매핑이 모든 Keycloak 버전·모든 client 설정에서 동일하게 동작한다는 명제 — 예시 토큰은 특정 realm/client 설정(`backend` realm, `rest-api` client) 기준
|
|
- `ROLE_` prefix 없이 `hasAuthority`/`hasRole` 을 섞어 쓸 때의 Spring Security 내부 동작 차이에 대한 설명 — 원문 미포함
|
|
- **내 프로젝트에 적용하려면 추가 확인이 필요한 것**:
|
|
- `feature-keycloak-spring-rs-role-mapping` 이 실제로 `resource_access` (client-level role) 까지 매핑할지, 아니면 D4 결정대로 `realm_access` 만 매핑할지 — 이 raw 의 `KeycloakJwtRolesConverter` 는 두 claim 을 모두 처리하므로 branch 결정과 범위가 다름(branch 는 realm role만, 이 raw 는 realm+resource 모두)에 주의
|
|
- 로컬 Keycloak 인스턴스에서 발급한 access token 의 `realm_access.roles` 실제 JSON 구조가 이 raw 의 예시 토큰과 일치하는지 확인
|
|
- `DelegatingJwtGrantedAuthoritiesConverter` 조합이 `feature-keycloak-spring-rs-role-mapping` 의 범위(§구현 가이드)에 실제로 필요한지 — branch 는 `@PreAuthorize` 대신 SecurityFilterChain matcher 를 우선하기로 결정했으므로 (D5), 이 raw 의 `.requestMatchers(...).hasAuthority(...)` 패턴과의 정합 재검토 필요
|
|
|
|
## 메모 / Notes
|
|
|
|
> 검증되지 않은 내 해석은 wiki source-summary 단계에서만.
|
|
|
|
- 원문은 realm-level role 과 resource(client)-level role 을 **모두** 매핑하는 구현(`KeycloakJwtRolesConverter`)을 제시하지만, `feature-keycloak-spring-rs-role-mapping` 의 D4 는 "realm role만 매핑 (resource_access 무시)"로 범위를 좁혔다 — 이 raw 를 인용할 때 **resource_access 부분은 branch 범위 밖**임을 명시해야 함 (OUT_OF_BRANCH_SCOPE 유사 주의).
|
|
- 원문 저자는 Keycloak 기본 설정(mapper 미변경)을 유지하는 쪽을 "minimally invasive" 라고 표현 — 이는 branch 의 "Keycloak mapper 커스터마이징 대신 Spring 쪽 컨버터로 흡수" 방향과 같은 트레이드오프 축으로 보인다(해석, 미검증).
|
|
- 저자는 GitHub 코드 링크(`ChristianHuff-DEV/secure-spring-rest-api-using-keycloak`)와 100% 테스트 커버리지를 주장하나, 이 raw 는 블로그 본문만 발췌·검증했고 GitHub 코드 자체는 self-grep 대상에 포함하지 않음 — 실제 사용 시 코드 diff 재확인 필요.
|
|
|
|
## Related / 관련
|
|
|
|
- 같은 주제 다른 raw:
|
|
- [[raw/official-docs/spring-security-resource-server-jwt]] — default `JwtGrantedAuthoritiesConverter` 가 `scope`/`scp` 만 자동 매핑한다는 공식 근거 (`SSRS-JWT-C4`) — 이 raw 의 C1과 짝을 이뤄야 `official-vendor-doc` 급 근거 완성
|
|
- 인용하는 branch:
|
|
- [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]]
|
|
- 인용한 wiki 요약: (미작성)
|