26 KiB
title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, parent_branch, related_projects, tags, created, target_merge, status_label, contract_packet_sha256
| title | source_type | status | id | kind | project | work_item | inherits | refines | overrides | depends_on | contract_packet | branch | parent_branch | related_projects | tags | created | target_merge | status_label | contract_packet_sha256 | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| branch / feature-keycloak-spring-rs-role-mapping (Keycloak role → Spring RBAC mapping) | branch-note | raw | BR-KEYCLOAK-CHILD-783CA54B | branch-child | keycloak-patterns-overview | WI-KEYCLOAK-PATTERNS-OVERVIEW-004 |
|
|
1 | feature-keycloak-spring-rs-role-mapping | feature-keycloak-spring-rs-audience-validator |
|
|
2026-05-25 | in-progress | dd660a8ddd1df3dabc7775e90818479fd5796ee4072d707242dc10c70827c701 |
branch: feature-keycloak-spring-rs-role-mapping (Keycloak role → Spring RBAC mapping)
Layer:
raw/branch-notes/— raw/branch-notes/feature-keycloak-spring-rs-audience-validator의 WI004 child branch. P3A는 실 구현 대상. 본 sub-sub는 학습 + 작업 plan 기록 — 실 구현은/home/donghyeon/workspace/keycloak-patterns/.
정합 노트 (2026-07-14 감사): 본 노트의 Spring RS 공통 셋업 +
aud검증 내용은 형제 raw/branch-notes/feature-keycloak-spring-rs-audience-validator 가 owner (중복 정리). 본 노트의 고유 책임 = role →@PreAuthorize(RBAC 인가) 이며, 이는 §5 deferred authZ 트랙이다(4 패턴 authN E2E 이후 착수). hub 분류: FOLD-IN(→ audience-validator 근거) + RBAC 부분 DEFERRED.
부모 (필수)
raw/branch-notes/feature-keycloak-spring-rs-audience-validator
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: foreign audience token 수용 실패를 재현하고 validator 적용 후 401을 검증한다
상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
DEC-KEYCLOAK-PATTERNS-OVERVIEW-SPA-CLIENT-001@1 |
AP1은 public client와 Authorization Code + PKCE를 사용한다 | AP1 Resource Server의 role claim 변환과 authorization에 적용한다 | raw/project-notes/keycloak-patterns-overview |
DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1 |
done-bar는 E2E success와 signature security failure 재현·해결 evidence다 | audience validator parent의 security verification contract를 승계한다 | raw/project-notes/keycloak-patterns-overview |
브랜치 지역 결정
기존 branch-local 결정은 아래
## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|
선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|
없음.
목표
검증을 통과한 Keycloak JWT의 realm_access.roles를 Spring ROLE_* authority로 변환하고 /api/admin에 RBAC를 강제한다. RS 공통 셋업과 audience 검증은 raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6을 선행 계약으로 소비한다.
면접 질문: "Keycloak이 발급한 JWT를 Spring에서 어떻게 검증하나요?"
→ "토큰 검증은 audience owner 계약을 따르고, 이 branch에서는 realm_access.roles의 nested claim을 custom converter로 읽어 ROLE_* authority로 바꿉니다. /api/admin은 admin-role을 요구하고 prefix 중복을 음성 테스트합니다."
- 이슈:
- PR: (별도 keycloak-patterns repo)
범위
포함 범위
JwtAuthenticationConverter— Keycloakrealm_access.roles→ SpringROLE_*/api/meendpoint:@AuthenticationPrincipal Jwt→ JWT claims 반환/api/adminendpoint:@PreAuthorize("hasRole('admin-role')")또는 SecurityFilterChain matcher- RBAC matcher/method-security 선택과 double-prefix 음성 테스트
제외 범위
- Opaque token introspection (Keycloak access token은 JWT)
- Custom JWT claim 변환 (예:
preferred_username→User도메인 객체 매핑) - Spring Session / 서버 측 세션
- Method-level security 정밀 튜닝
- Spring RS 의존성·
issuer-uri·JwtDecoder·audience value/validator·CORS → raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6. expected audience는 owner의backend-client-id를 소비하며 여기서 재명세하지 않는다
근거 (필수, 최소 1개+)
- raw/official-docs/spring-security-resource-server-jwt — Spring Security Resource Server JWT 공식
- raw/official-docs/spring-security-authorize-http-requests — RBAC enforcement location Alternative A (
authorizeHttpRequests+requestMatchers(...).hasRole(...)) 공식 근거 — request-level 모델링,AuthorizationFiltertiming, path-only matching 한계 - raw/company-tech-blogs/keycloak-jwt-role-extraction-betweendata —
personal-blog(Christian Huff). 커스텀Converter<Jwt, Collection<GrantedAuthority>>로realm_accessclaim 을 읽어ROLE_prefix 로 변환하고DelegatingJwtGrantedAuthoritiesConverter로 default scope converter 와 결합하는 구현 사례.engineering-blog강도 — 공식 best practice 아님, D4 구현 detail 참고용 - raw/official-docs/spring-security-method-security — RBAC enforcement location Alternative B(
@EnableMethodSecurity+@PreAuthorize("hasRole('admin-role')")) 채택 근거 + unannotated method 미보호 CRITICAL backstop 경고(catch-allHttpSecurity규칙 필수) - raw/official-docs/spring-security-nested-authorities-claim-issue-15201 —
JwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("realm_access.roles")nested claim 미지원 known-limitation + custom converter 워크어라운드 +ExpressionJwtGrantedAuthoritiesConverter(6.4+) 공식 확인 (GitHub Issue #15201, vendor 저장소) - raw/official-docs/spring-security-authorization-defense-in-depth — RBAC enforcement location Alternative C(request-level + method-level 동시 사용 = defense in depth) 결정의 벤더 공식 근거
- raw/official-docs/spring-security-authorization-architecture —
ROLE_prefix 는 Spring Security 기본값(role-based rule 이ROLE_자동 부착,SS-AUTHZ-ARCH-C5) —hasRole("admin-role")double-prefix 계약(§구현 가이드 3)의 공식 근거
TODO
- 선행 계약 확인: raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6이 정상·wrong-audience E2E를 통과 — 등급:
planned JwtAuthenticationConverter빈:realm_access.roles→SimpleGrantedAuthority("ROLE_" + role)매핑 — 등급:planned@RestControllerMeController:GET /api/me→@AuthenticationPrincipal Jwt jwt→Map.of("sub", jwt.getSubject(), "preferred_username", jwt.getClaim("preferred_username"), "roles", jwt.getClaim("realm_access"))반환 — 등급:planned@RestControllerAdminController:GET /api/admin—@PreAuthorize("hasRole('admin-role')")또는 matcher 기반 — 등급:planned- Dockerfile (multi-stage: gradle build → JRE 21 runtime) — 등급:
planned - 로컬 검증: regular-user 토큰으로
/api/me200,/api/admin403 — 등급:planned - 로컬 검증: admin-user 토큰으로
/api/admin200 — 등급:planned - 선행 owner의 wrong-audience 401 결과를 consume하고 본 branch에서는 RBAC 200/403만 추가 검증 — 등급:
planned - 로컬 검증:
hasRole("admin-role")(prefix 자동) vshasRole("ROLE_admin-role")(double-prefix 버그) 대조 — 등급:planned
진행 중 메모
- RS/audience prerequisite: raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D4/D6이 owner다. 본 branch는 검증을 통과한 JWT만 입력으로 받는다.
- role mapping: Keycloak token claim 구조 —
realm_access: { roles: [admin-role, user-role] }. resource_access는 client별 role (out of scope). /api/me응답에 raw JWT claims 노출 신중: 학습 목적이라 OK, prod에서는 필요한 claim만 반환.- Spring Boot 3 + Spring Security 6 기준 lambda DSL 사용. 옛 fluent API는 deprecated.
- (2026-07-18 자동조사)
setAuthoritiesClaimName("realm_access.roles")는 nested 미지원: 공식 확인된 사실 = nestedrealm_access.roles는 이 API 로 못 읽고 customConverter또는 SS ≥6.4 의ExpressionJwtGrantedAuthoritiesConverter로만 처리(SS-15201-C2/C3). 왜 실패하는지의 내부 원리("dot 을 경로 구분자로 안 쓰고 top-level claim 을 literal lookup")는 추정 — SS-15201 는 이를 증명하지 않으며 소스/Javadoc 별도 확인 필요. 관측 결과는 0 authority(silent 403) 로 예상. §Decision Evidence Map D6 + §구현 가이드 1 참조.
결정 사항 (decisions)
- 2026-07-18 (delegated): RS 셋업·audience 검증은 raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6을 따른다.
- 2026-05-25: realm-global 권한만 필요한 baseline에서는 realm role만 매핑한다. client-specific 권한 namespace가 필요하면
resource_access.<client>.rolesvariant를 별도 결정한다. client 개수 자체는 선택 근거가 아니다. - 2026-05-25:
@PreAuthorize대신 SecurityFilterChain matcher 우선. 이유: 권한 정책 한 곳 집중 → 면접 답변 일관성. - 2026-07-18: realm role → authority 매핑에 custom
Converter<Jwt, Collection<GrantedAuthority>>채택 (setAuthoritiesClaimName폐기). 이유:setAuthoritiesClaimName은 nestedrealm_access.roles를 파싱 못함(literal top-level lookup, silent 403). 대안: Boot ≥3.4 로 pin 시ExpressionJwtGrantedAuthoritiesConverter+ SpEL 한 줄. 근거: SS-15201, SSRS-JWT-C4, betweendata 사례. (자동조사/branch-spec) - 2026-07-18: D5(RBAC 강제 지점)의
UNSUPPORTED_DECISION해소 — 근거 확보. 기본 A(HTTP matcher), 조건부 B(@PreAuthorize+catch-all)/C(defense-in-depth). 근거: 공식 authorize-http-requests / method-security / features-authorization. (자동조사/branch-spec)
결정-근거 매핑
각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시.
Decision ID는 본 branch-note 안에서 안정적으로 유지.선택 조건열: "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면N/A. 정합 (2026-07-14): RS-common(D1·D2·D3)은 형제 raw/branch-notes/feature-keycloak-spring-rs-audience-validator 가 owner — 본 노트 in-scope 는 role→RBAC(D4·D5·D6). 상세는 §Audit & Findings.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | DELEGATED — raw/branch-notes/feature-keycloak-spring-rs-audience-validator D6의 RS 공통 셋업을 consume | RBAC는 검증 완료 JWT 위에 얹힘 | owner D6 | delegated |
본 branch에서 버전·decoder wiring을 재명세하지 않음 |
| D2 | DELEGATED — raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D4의 audience 계약(backend-client-id)을 consume |
expected audience 변경은 owner에서만 | owner D1/D4 | delegated |
본 branch는 audience 구현·테스트를 복제하지 않음 |
| D3 | DELEGATED — raw/branch-notes/feature-keycloak-spring-rs-audience-validator D6의 bearer RS 실행 계약을 consume | RBAC 입력 전제 | owner D6 | delegated |
session/CORS 세부를 재명세하지 않음 |
| D4 | realm-global 권한이면 realm_access.roles만 매핑; client-specific 권한이 필요하면 resource_access.<client>.roles variant |
선택 기준은 권한 namespace다. client 수가 많아도 공통 권한이면 realm role을 유지할 수 있고, client별 격리가 필요하면 client role을 추가한다 | raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C4 |
official-vendor-doc + project policy |
현재 authZ는 deferred. 실제 client별 권한 요구를 확정하기 전 realm-only를 외부 경험으로 승격 금지 |
| D5 | RBAC 강제 지점 — 기본 SecurityFilterChain matcher(A), 조건부 B/C |
기본=A(authorizeHttpRequests matcher): endpoint 소수 + role↔URL 안정 + "정책 한 곳 집중/면접 일관성" 목표. B(@PreAuthorize, A catch-all 유지 필수): 파라미터/소유권 기반 판단 또는 비-HTTP 진입점. C(A+B 병행=defense-in-depth): 프로덕션 노출 + matcher/annotation 누락이 실제 위협일 때 |
raw/official-docs/spring-security-authorize-http-requests.md#SS-AUTHZ-HTTP-C1 (request-level 모델링 — /admin 하위 authority), raw/official-docs/spring-security-authorize-http-requests.md#SS-AUTHZ-HTTP-C3 (AuthorizationFilter 가 DispatcherServlet/컨트롤러 실행 전 차단), raw/official-docs/spring-security-method-security.md#SPRING-MS-C5 (unannotated method 미보호 → B 시 catch-all 필수), raw/official-docs/spring-security-authorization-defense-in-depth.md#SS-AUTHZ-DID-C1 (request+method = defense in depth) |
official-vendor-doc (+ personal/company-blog corroborate: Okta·Marco Behler·howtodoinjava — 미아카이브, official 로 충분) |
C 채택 시 두 계층 role 조건 동기화 미스매치가 "단일 설명 위치" 목표 훼손; A 단독 시 URL glob drift(SS-AUTHZ-HTTP-C4 path-only); B 단독 시 미어노테이트/self-invocation 무보호 |
| D6 | Keycloak realm_access.roles → GrantedAuthority 매핑 메커니즘 = 수동 custom Converter<Jwt, Collection<GrantedAuthority>> (setAuthoritiesClaimName 폐기) |
nested claim(realm_access.roles)이라 flat-claim 전용 setAuthoritiesClaimName 는 확정 실패(nested 미지원 = SS-15201 공식; 내부 lookup 원리는 추정 → §진행 중 메모). 대안: Boot ≥3.4 / SS ≥6.4 로 pin 가능하면 ExpressionJwtGrantedAuthoritiesConverter + SpEL [realm_access][roles] (커스텀 클래스 없이 한 줄) |
raw/official-docs/spring-security-nested-authorities-claim-issue-15201.md#SS-15201-C2 (custom JwtGrantedAuthoritiesConverter 구현이 nested role 추출에 필요), raw/official-docs/spring-security-nested-authorities-claim-issue-15201.md#SS-15201-C3 (ExpressionJwtGrantedAuthoritiesConverter fix, milestone 6.4.0), raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C4 (default 는 scope/scp 만 매핑), raw/company-tech-blogs/keycloak-jwt-role-extraction-betweendata.md#KC-ROLE-BD-C1 (custom Converter 로 realm_access 읽는 구현 예) |
official-vendor-doc + engineering-blog |
jwt.getClaim() 이 claim 부재 시 빈 컬렉션 반환(silent 403)은 소스 self-grep 전까지 needs-confirmation; betweendata 예제는 ROLE_realm_ prefix + resource role 도 매핑(D4 범위 밖) → 본 브랜치는 ROLE_ + realm-only 로 조정 |
구현 가이드
본 §는 이 branch 의 in-scope = RBAC/role 매핑 트랙(D4·D5·D6) 만 구체화한다. RS 공통 셋업(Boot 의존성·
issuer-uri·JwtDecoder·aud검증 = D1·D2·D3)은 형제 raw/branch-notes/feature-keycloak-spring-rs-audience-validator 가 owner(2026-07-14 감사) → 여기서 재명세하지 않고 §엣지·실패·의존 "다른 계약 의존" 으로 링크(R3 OUT_OF_BRANCH_SCOPE).keycloak-patternsrepo 부재(NO_GROUND_TRUTH, §Audit) → 아래 전부planned. 실 구현 후 코드 grep 으로 등급 승급.
1. Realm role → GrantedAuthority 매핑 (핵심)
Trace: D4(realm-only) + D6(custom Converter 메커니즘) —
SS-15201-C2/SS-15201-C3,SSRS-JWT-C4,KC-ROLE-BD-C1.
- UNSUPPORTED_IMPL_DECISION: (1) authority prefix 문자열 =
ROLE_(betweendata 사례는ROLE_realm_) —hasRole("admin-role")이ROLE_admin-role을 기대하므로ROLE_채택. trade-off: betweendata 예제와 불일치하나 표준hasRole계약에 정합(§3). (2) claim 부재 시 빈 컬렉션 반환(null-safe) — 근거 raw 는 방어 코드 형태를 규정 안 함, silent-403 진단성 위한 임의 선택.
| 항목 | 명세 |
|---|---|
| 클래스 | KeycloakRealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> (별도 파일 또는 SecurityConfig static nested class) |
| 읽기 | Map<String,Object> realmAccess = jwt.getClaimAsMap("realm_access"); → realmAccess.get("roles") 를 Collection<String> 으로 |
| 방출 | 각 role → new SimpleGrantedAuthority("ROLE_" + role) |
| null-safety | realmAccess == null 또는 roles 가 Collection 아니면 → Collections.emptyList() |
| wiring | JwtAuthenticationConverter jac = new JwtAuthenticationConverter(); jac.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter()); → .oauth2ResourceServer(o -> o.jwt(j -> j.jwtAuthenticationConverter(jac))) |
| 대안(버전 pin 시) | Boot ≥3.4 / SS ≥6.4 → 커스텀 클래스 대신 ExpressionJwtGrantedAuthoritiesConverter + SpEL "[realm_access][roles]" (SS-15201-C3) — 단 본 브랜치는 버전 미pin 이라 custom Converter 를 기본으로 함 |
2. /api/admin RBAC 강제 지점 (기본 A)
Trace: D5 —
SS-AUTHZ-HTTP-C1/SS-AUTHZ-HTTP-C3,SPRING-MS-C5(backstop),SS-AUTHZ-DID-C1(조건부 C).
- UNSUPPORTED_IMPL_DECISION: URL glob
/api/admin/**+ role 명admin-role— 공식 예시는 illustrative(SS-AUTHZ-HTTP-C1"Does not prove admin-role name"); glob/명명은 프로젝트 임의 결정(Keycloak realm role 명명은feature-keycloak-realm-client-export소관).
| 항목 | 명세 |
|---|---|
| 강제(A) | .authorizeHttpRequests(a -> a.requestMatchers("/api/admin/**").hasRole("admin-role").anyRequest().authenticated()) |
/api/me |
별도 role 없이 authenticated() (위 anyRequest() 로 커버) |
| 타이밍 | AuthorizationFilter 가 DispatcherServlet 이전 실행 → 컨트롤러 도달 전 차단(SS-AUTHZ-HTTP-C3) |
| 조건부 승격(B) | 파라미터/소유권 기반 필요 시 @EnableMethodSecurity + @PreAuthorize("hasRole('admin-role')"), 단 anyRequest().authenticated() catch-all 유지 필수(SPRING-MS-C5 — unannotated method 무보호 방지) |
| 조건부 승격(C) | 프로덕션 노출 시 A+B 병행(defense in depth, SS-AUTHZ-DID-C1) — 두 계층 role 조건 동기화 규율 전제 |
3. hasRole prefix 계약 (double-prefix 함정)
Trace: D6 —
SS-AUTHZ-ARCH-C5(ROLE_자동 prefix = Spring Security 기본값, 공식),KC-ROLE-BD-C2(double-prefix 위험 사례).
hasRole("admin-role") 은 내부적으로 ROLE_ 를 자동 prefix (SS-AUTHZ-ARCH-C5) → §1 converter 가 이미 ROLE_admin-role 을 만들었으므로 인자는 prefix 없이 hasRole("admin-role") 로 호출한다. hasRole("ROLE_admin-role") 로 부르면 ROLE_ROLE_admin-role 을 조회 → admin 이 항상 403. §Claims To Verify + TODO 에 이 self-check(prefix 유무 대조) 추가.
엣지·실패·의존
R4(깊이 게이트) 캡처용. 정상 경로 외 실패/엣지/다른 계약 의존을 미리 열거.
- 실패·엣지 경로:
- nested claim silent failure —
setAuthoritiesClaimName("realm_access.roles")사용 시 literal top-level lookup 실패 → 0 authority → 모든hasRolefalse → 전 요청 403, 예외/로그 없음(SS-15201-C2). 기대 동작: custom converter(§1)로 회피 +/api/me응답에ROLE_user-role존재 확인. realm_accessclaim 부재 — Keycloak client 에 realm-role mapper 없으면 claim 누락 → converter empty → 403. 기대: null-safe converter(§1) + realm role mapper 설정(→ 아래 의존).- double-prefix —
hasRole("ROLE_admin-role")오용 시ROLE_ROLE_admin-role→ admin 항상 403(§3). - unannotated-method gap (조건부 B 채택 시) — 어노테이션 누락 endpoint 무보호. catch-all
anyRequest().authenticated()유지로 방어(SPRING-MS-C5).
- nested claim silent failure —
- 다른 계약 의존:
- raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6 — RS 공통 셋업과
backend-client-idaudience 검증 owner. 본 role 매핑은 검증 완료 JWT 위에 얹힌다. - raw/branch-notes/feature-keycloak-realm-client-export
#D5— realm-export.json 이 realm role(admin-role/user-role) 정의 + userrealmRoles부여를 담음(그 노트 §TODO Role 생성). Keycloak 기본 realm-roles protocol mapper 가 이를 token 의realm_access.roles로 실음 → export 가 role 을 안 담으면 본 매핑은 빈 authority(위 "claim 부재" 엣지). - raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch
#D1—KC_HOSTNAME/iss문자열 일치(token 이 통과해야 role 매핑 단계에 도달).
- raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6 — RS 공통 셋업과
Audit & Findings
§2 ground-truth 확인 + 결정 정합 감사 결과. 자동 rewrite 대상 아님(surface + 정합 권고).
- BROKEN_CODE_DRIFT (surface-only, read-only 권고): 인용 근거 raw/official-docs/spring-security-resource-server-jwt 의 §"권한 추출 customize (해석)" 코드가
setAuthoritiesClaimName("realm_access.roles")를 사용 — nested claim 을 파싱하지 못해 작동하지 않는 패턴(SS-15201-C2). 그 raw 는 이미 "추가 확인 필요" 로 flag 되어 있으나, 코드 블록 자체에 "nested 미지원 → custom Converter /ExpressionJwtGrantedAuthoritiesConverter(6.4+) 필요" caveat 추가를 권고. 해당 raw 는 별도 소유 → 자동 수정 안 함(정합 권고만). - DELEGATION (2026-07-14 감사 정합): RS 공통 셋업 +
aud(D1·D2·D3)는 형제 raw/branch-notes/feature-keycloak-spring-rs-audience-validator 가 owner. 본 노트 in-scope = role→RBAC(D4·D5·D6) = deferred authZ 트랙(raw/project-notes/keycloak-patterns-overview §Deferred — 4 패턴 authN E2E 후 착수). D1·D3 가 여기서UNSUPPORTED_DECISION인 것은 RS-common(sibling 소유 rationale)이기 때문 — 본 브랜치 추가 조사 대상 아님(R3 OUT_OF_BRANCH_SCOPE). - NO_GROUND_TRUTH: 실 구현 대상 repo
/home/donghyeon/workspace/keycloak-patterns/부재(2026-07-18 확인) → 본 노트 모든 항목planned/documented-only.actually-implemented주장은 코드 대조 불가이므로 하지 않음(§구현 가이드는 사전 명세일 뿐).
검증해야 할 주장
공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| RS/audience prerequisite가 완료된 JWT만 RBAC converter에 도달 | 선행 owner repo가 아직 미구현 | raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6 E2E 결과를 consume하고 본 branch 테스트 fixture의 전제로 기록 | planned (delegated) |
custom KeycloakRealmRoleConverter(§1)가 실제 Keycloak token 에서 ROLE_user-role/ROLE_admin-role authority 를 방출 (setAuthoritiesClaimName 은 D6/SS-15201 로 이미 폐기 확정) |
메커니즘은 확정됐으나 로컬 실동작 + 실제 token 의 realm_access.roles 구조/composite role 확장 여부 미확인 |
regular-user 로 token 발급 → backend /api/me 응답에서 ROLE_user-role granted authority 존재 확인 |
planned |
@PreAuthorize("hasRole('admin-role')") / .hasRole("admin-role") 가 converter 의 ROLE_admin-role 과 정확히 매칭(double-prefix 없음) |
hasRole 이 ROLE_ 를 자동 prefix — converter 도 ROLE_ 를 붙이므로 인자에 ROLE_ 재기입 시 ROLE_ROLE_ 버그(KC-ROLE-BD-C2) |
admin-user token 으로 /api/admin 200 확인 → 인자를 hasRole("ROLE_admin-role") 로 바꿔 403 되는지 대조 |
planned |
마주친 문제
- (선행 계약) audience 발급·검증 문제는 raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D4에서 추적한다. 본 branch는 검증 완료 뒤의 RBAC만 소유한다.
- (구현 시작 후 추가)
issuer-uri가 backend 기동 시점에 reachable하지 않으면 Spring startup 실패 — docker-composedepends_on healthy로 해결.
묶음
- raw/company-tech-blogs/keycloak-jwt-role-extraction-betweendata
- raw/official-docs/spring-security-authorization-defense-in-depth
- raw/official-docs/spring-security-authorize-http-requests
- raw/official-docs/spring-security-method-security
- raw/official-docs/spring-security-nested-authorities-claim-issue-15201
- raw/official-docs/spring-security-resource-server-jwt
본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
오류 기록 (이 sub-sub-branch 작업 중 발생)
- (없음 — 현재 documented-only 단계)
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (없음 — Phase 3 실 구현 단계에 누적)
관련 일일 노트
완료 후 정리
로컬
curl검증 통과 시planned→actually-implemented/locally-verified승급.
- PR 링크: (별도 keycloak-patterns repo)
- 리뷰 메모:
- 머지 결과 / 배포 환경: 로컬 docker-compose / 단일 EC2 시뮬레이션
- wiki 추출 대상:
actually-implemented항목: (구현 후 채움)locally-verified항목: (구현 후 채움)prod-verified항목: (없음)
- 추출하지 않을 항목: 현재 전부
planned.