docs(keycloak): adopt the decomposition contract, fix the redirect URI, strip evaluative prose
- 계약 채택 — 독자 질문, 후보 29건(PROMOTE 24 · MERGE_INTO 4 · KEEP_IN_SSOT 1). 게시 중 17건은 전부 유지. 저장소 keycloak-pattern 은 패턴 넷이 브랜치로 갈라져 있어 revisions 로 tip 넷을 적었다. keycloak-session-store 는 같은 저장소 @ cdac9b8 - 게시된 기록의 redirect_uri 가 SSOT·코드와 달랐다 — OAuth2callback.html → callback.html (frontend/src/app.js 에서 확인). 계약 title 이 기록과 다른 7건도 기록 쪽으로 맞췄다 - 미작성 1건 작성 — 패턴 검증을 실제로 돌릴 때의 안전한 순서(Reference) - 리뷰 100건 반영 — 설명 뒤에 붙은 평가·차례 예고·독자 오해 가정·작성 지시를 지웠다. 삭제가 남긴 조각 4건을 고치고, 원래부터 잘려 있던 로컬 미리보기 라벨 1건도 닫았다 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
62520a4dce
commit
4d50bb939a
+71
-65
@@ -11,124 +11,130 @@ version: 32
|
||||
questionStatus: OPEN
|
||||
studio: "https://hyeonworks.com/studio/documents/18a5cde2-dd1e-4bff-9f1c-997577ae438f/edit"
|
||||
public: "https://hyeonworks.com/questions/bff-session-authorized-client-store"
|
||||
sourceRevision: keycloak-patterns-lab@2026-08
|
||||
source:
|
||||
- final/document.md#검토한-선택지와-막힌-지점-ap3
|
||||
- final/document.md#문제를-어렵게-만든-제약-학습-환경
|
||||
- final/document.md#얻은-것-잃은-것-적용하지-않을-때-ap3
|
||||
---
|
||||
|
||||
# BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
|
||||
|
||||
Application Session과 Authorized Client는 저장하고 조회하는 기준이 서로 다르다.
|
||||
Session은 `session ID`를 기준으로 조회하지만, Authorized Client는 `client registration 이름`과 `principal name`을 기준으로 조회한다.
|
||||
그래서 두 상태를 반드시 같은 저장소에 보관해야 하는 것은 아니며, 각각의 조회 방식과 운영 요구사항에 맞게 저장 구조를 결정해야 한다.
|
||||
현재 BFF의 세션과 Authorized Client는 프로세스 메모리에 저장된다. 재시작과 레플리카 이동 뒤에도 로그인 상태를 유지하려면 두 상태를 어디에 저장할지 정해야 한다.
|
||||
|
||||
현재 Shared Store의 후보로는 Redis를 우선 생각하고 있지만 아직 최종 저장소로 결정한 것은 아니다.
|
||||
특히 Access Token과 Refresh Token을 Redis에 저장할 경우 Token을 어떤 방식으로 암호화할지, Session과 Token의 만료 시간을 어떻게 맞출지, Logout할 때 Session과 Authorized Client가 모두 정상적으로 제거되는지까지는 확인하지 않았다.
|
||||
BFF(Backend for Frontend)가 서버에 들고 있는 상태는 둘이다. 하나는 브라우저의 로그인 세션이고,
|
||||
다른 하나는 Keycloak에서 받은 액세스 토큰과 리프레시 토큰을 담아 두는 Authorized Client다.
|
||||
세션은 `session ID`로 찾지만 Authorized Client는 `client registration 이름`과 `principal name`으로 찾는다.
|
||||
|
||||
따라서 현재 단계에서는 Redis를 저장소 후보로 작성하고, Token 보호와 만료 처리, Logout 시 상태 정리까지 검증한 뒤 실제 저장 구조를 결정한다.
|
||||
찾는 열쇠가 이렇게 다르니 두 상태를 반드시 한 저장소에 담아야 하는 것은 아니다.
|
||||
각각의 조회 방식과 운영 요구사항에 맞게 저장 구조를 따로 정할 수 있다.
|
||||
|
||||
공유 저장소 후보로 Redis를 우선 보고 있지만 아직 고르지 않았다.
|
||||
액세스 토큰과 리프레시 토큰을 Redis에 담는다면 토큰을 어떤 방식으로 암호화할지, 세션과 토큰의 만료 시간을 어떻게 맞출지,
|
||||
로그아웃할 때 세션과 Authorized Client가 모두 지워지는지를 확인해야 하는데 아직 확인하지 않았다.
|
||||
|
||||
토큰 보호와 만료 처리, 로그아웃 뒤 상태 정리까지 검증한 다음에 저장 구조를 정한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가**
|
||||
이 질문에서 저장소 부분만 떼어 낸 것이다.
|
||||
- **Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정**
|
||||
session과 authorized client의 열쇠가 다르다는 사실의 출처다.
|
||||
세션과 Authorized Client를 찾는 열쇠가 다르다는 사실의 출처다.
|
||||
- **BFF 인증 구조 설계 기준**
|
||||
이 기준의 저장소 항목이 이 질문의 답을 기다린다.
|
||||
이 기준의 저장소 항목은 여기에 답이 나와야 채울 수 있다.
|
||||
- **Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가**
|
||||
저장소를 공유한 뒤에야 replica 경쟁이 재현된다.
|
||||
저장소를 공유한 다음에야 레플리카 경쟁을 재현할 수 있다.
|
||||
|
||||
## 사실
|
||||
|
||||
- 현재 구성에 Spring Session과 Redis, JDBC repository, 암호화 token store가 없다.
|
||||
- 현재 HttpSession은 servlet container의 in-memory 구현을 사용하므로 해당 process가 종료되면 session 데이터도 유지되지 않는다.
|
||||
- OAuth2AuthorizedClientService도 자동구성이 고르는 in-memory 구현이다.
|
||||
- session은 session ID로 조회하고 authorized client는 registration 이름과 principal name으로 조회한다.
|
||||
두 저장 구조를 shared store로 전환할 때 각각 따로 설계해야 한다.
|
||||
- authorized client manager에 authorization-code와 refresh-token provider가 함께 구성돼 있어서,
|
||||
저장소를 공유하게 되면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있게 된다.
|
||||
- 현재 구성에는 Spring Session도, Redis도, JDBC 저장소도, 토큰을 암호화해 담는 저장소도 없다.
|
||||
- HttpSession은 서블릿 컨테이너의 메모리 구현을 쓰기 때문에, 그 프로세스가 종료되면 세션 데이터도 같이 사라진다.
|
||||
- OAuth2AuthorizedClientService도 Spring Boot 자동구성이 고르는 메모리 구현이다.
|
||||
- 세션은 session ID로 조회하고 Authorized Client는 registration 이름과 principal name으로 조회한다.
|
||||
두 저장 구조를 공유 저장소로 옮길 때는 각각 따로 설계해야 한다.
|
||||
- Authorized Client 매니저에는 authorization-code와 refresh-token provider가 함께 구성돼 있어서,
|
||||
저장소를 공유하면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있다.
|
||||
|
||||
## 가정
|
||||
|
||||
- 두 상태를 같은 저장소에 둘 필요는 없다.
|
||||
- 저장된 refresh token을 평문으로 두면 안 된다.
|
||||
- session 만료와 token 만료 중 하나가 먼저 오게 되면 그 순간의 동작이 정의돼 있어야 한다.
|
||||
예를 들면 token이 만료됐을 때 사용자의 로그인 상태가 여전히 유지되는지.
|
||||
- 저장한 리프레시 토큰을 평문으로 두면 안 된다.
|
||||
- 세션 만료와 토큰 만료 중 하나가 먼저 오면 그 순간에 무엇이 일어날지 정해 두어야 한다.
|
||||
토큰이 만료됐을 때 사용자의 로그인 상태가 유지되는지가 그런 예다.
|
||||
|
||||
## 미지수
|
||||
|
||||
- Redis와 JDBC 중 무엇이 이 상태의 접근 패턴에 맞는가.
|
||||
- Redis와 JDBC 중 어느 쪽이 이 상태의 접근 패턴에 맞는가.
|
||||
요청마다 읽는 값과 가끔 읽는 값이 섞여 있다.
|
||||
- session과 authorized client를 같은 store에 둘지 나눌지.
|
||||
- 암호화 key를 어디에 두고 어떻게 교체하게 되는가. 교체하는 동안 이전 key로 저장된 값은 어떻게 읽는가.
|
||||
- session TTL과 refresh token 수명 중 어느 것을 기준으로 만료를 맞추게 되는가.
|
||||
- session과 Authorized Client 두 store를 logout에서 어떻게 한 번에 지우게 되는가.
|
||||
- sticky session이 durable store의 대안이 되는가 보완이 되는가.
|
||||
- 세션과 Authorized Client를 한 저장소에 둘지 나눌지.
|
||||
- 암호화 키를 어디에 두고 어떻게 교체하는가. 교체하는 동안 이전 키로 저장한 값은 어떻게 읽는가.
|
||||
- 세션 TTL과 리프레시 토큰 수명 중 어느 쪽을 기준으로 만료를 맞추는가.
|
||||
- 로그아웃 한 번으로 세션과 Authorized Client 두 저장소를 어떻게 같이 지우는가.
|
||||
- 스티키 세션이 영속 저장소의 대안인가 보완인가.
|
||||
|
||||
## 제약
|
||||
|
||||
- authorized client의 조회 시 session ID가 없어서 session만 공유해도 같은 사용자의 여러 session이 같은 token을 보게 된다.
|
||||
- 현재 테스트에는 저장소 관련 계약이 없다.
|
||||
- 모든 UI 요청이 BFF를 지나기 때문에 저장소 지연이 화면 지연으로 바로 드러나게 된다.
|
||||
- Authorized Client를 찾을 때는 session ID를 쓰지 않아서, 세션만 공유해도 같은 사용자의 여러 세션이 같은 토큰을 본다.
|
||||
- 현재 테스트에는 저장소 계약이 없다.
|
||||
- 모든 화면 요청이 BFF를 지나기 때문에 저장소가 느려지면 화면도 바로 느려진다.
|
||||
|
||||
## 선택지
|
||||
|
||||
### 1. session과 authorized client를 모두 Redis에 둔다
|
||||
### 1. 세션과 Authorized Client를 모두 Redis에 둔다
|
||||
|
||||
Spring Session Redis와 Redis 기반 Authorized Client 저장소를 사용하면 여러 애플리케이션 인스턴스가 같은 Session과 Authorized Client 정보를 조회할 수 있다.
|
||||
상태가 특정 인스턴스의 메모리에 묶이지 않으므로 서버가 재시작되거나 요청이 다른 Replica로 전달되는 환경에서도 인증 상태를 공유하기 쉬워진다.
|
||||
Spring Session Redis와 Redis 기반 Authorized Client 저장소를 쓰면 여러 애플리케이션 인스턴스가 같은 세션과 Authorized Client를 조회할 수 있다.
|
||||
상태가 특정 인스턴스의 메모리에 묶이지 않으니 서버가 재시작되거나 요청이 다른 레플리카로 전달되는 환경에서도 인증 상태를 이어 쓰기 쉬워진다.
|
||||
|
||||
Session과 Authorized Client에 설정된 유효 시간에 따라 Redis의 TTL을 이용해 저장된 상태를 만료시키는 구조도 구성할 수 있다.
|
||||
다만 어떤 상태를 얼마 동안 유지할지는 애플리케이션의 Session 정책과 OAuth Token의 수명에 맞춰 별도로 정해야 한다.
|
||||
세션과 Authorized Client에 설정한 유효 시간에 맞춰 Redis의 TTL로 저장한 상태를 만료시키는 구조도 짤 수 있다.
|
||||
다만 어떤 상태를 얼마 동안 유지할지는 애플리케이션의 세션 정책과 OAuth 토큰 수명에 맞춰 따로 정한다.
|
||||
|
||||
이 구성에서는 인증 경로가 Redis의 가용성에 의존하게 된다.
|
||||
Redis에 장애가 발생했을 때 기존 Session과 Authorized Client를 조회하지 못하는 상황을 어떻게 처리할지 정해야 하며,
|
||||
장애 복구와 데이터 유지 방식도 함께 고려해야 한다.
|
||||
대신 인증 경로가 Redis의 가용성에 매인다.
|
||||
Redis에 장애가 났을 때 기존 세션과 Authorized Client를 읽지 못하는 상황을 어떻게 처리할지 정해야 하고, 장애 복구와 데이터 유지 방식도 같이 본다.
|
||||
|
||||
또한 Access Token과 Refresh Token을 Redis에 저장한다면 저장된 Token을 어떤 방식으로 보호할지도 결정해야 한다.
|
||||
Token을 암호화해서 저장할지, 암호화한다면 Key를 어디에 보관하고 어떻게 교체할지까지 저장소 설계에 포함한다.
|
||||
액세스 토큰과 리프레시 토큰을 Redis에 담는다면 담은 토큰을 어떻게 보호할지도 정해야 한다.
|
||||
암호화해서 저장할지, 암호화한다면 키를 어디에 보관하고 어떻게 교체할지까지 저장소 설계에 포함한다.
|
||||
|
||||
### 2. session과 authorized client를 모두 JDBC에 둔다
|
||||
### 2. 세션과 Authorized Client를 모두 JDBC에 둔다
|
||||
|
||||
JDBC 기반 저장소를 사용하면 이미 운영 중인 관계형 DB에 Session과 Authorized Client 정보를 저장할 수 있다.
|
||||
JDBC 기반 저장소를 쓰면 이미 운영 중인 관계형 DB에 세션과 Authorized Client를 담을 수 있다.
|
||||
|
||||
인증 과정에서 Session이나 Authorized Client를 조회할 때마다 DB 접근이 발생하므로, 인증 요청이 기존 관계형 DB의 가용성과 성능에 영향을 받게 된다.
|
||||
요청량이 증가했을 때 Session 조회가 지연되지 않는지 확인하고, 인증 관련 조회가 기존 애플리케이션 쿼리와 서로 영향을 주지 않는지도 확인해야 한다.
|
||||
인증 과정에서 세션이나 Authorized Client를 조회할 때마다 DB 접근이 일어나므로, 인증 요청이 기존 관계형 DB의 가용성과 성능에 매이게 된다.
|
||||
요청량이 늘었을 때 세션 조회가 지연되지 않는지, 인증 조회와 기존 애플리케이션 쿼리가 서로 영향을 주지 않는지 확인해야 한다.
|
||||
|
||||
또한 만료된 Session과 Authorized Client 데이터가 계속 쌓이지 않도록 정리하는 방법과 주기를 정해야 한다.
|
||||
JDBC를 고르면 기존 DB 운영 체계를 그대로 쓰면서 조회 지연과 DB 부하, 만료 데이터 정리까지 같이 관리하게 된다.
|
||||
만료된 세션과 Authorized Client 데이터가 계속 쌓이지 않도록 정리하는 방법과 주기도 정한다.
|
||||
|
||||
### 3. session만 공유하고 sticky session을 쓴다
|
||||
### 3. 세션만 공유하고 스티키 세션을 쓴다
|
||||
|
||||
Session Affinity를 사용하면 같은 사용자의 요청을 가능한 한 동일한 애플리케이션 인스턴스로 전달할 수 있으므로 기존 구조의 변경을 줄일 수 있다.
|
||||
세션 어피니티를 쓰면 같은 사용자의 요청을 되도록 같은 애플리케이션 인스턴스로 보낼 수 있어서 기존 구조를 덜 건드린다.
|
||||
|
||||
하지만 이것만으로 인증 상태가 여러 인스턴스에 공유되는 것은 아니다.
|
||||
Authorized Client가 여전히 특정 애플리케이션 인스턴스의 메모리에 저장되어 있다면, 요청이 다른 인스턴스로 전달되거나 해당 인스턴스가 종료되었을 때 기존 Token 정보를 조회할 수 없다.
|
||||
하지만 이것만으로 인증 상태가 여러 인스턴스에 공유되지는 않는다.
|
||||
Authorized Client가 여전히 특정 애플리케이션 인스턴스의 메모리에 있다면, 요청이 다른 인스턴스로 전달되거나 그 인스턴스가 종료됐을 때 기존 토큰 정보를 찾지 못한다.
|
||||
|
||||
Session Affinity는 요청을 특정 인스턴스로 보내는 방법이다. Session과 Authorized Client를 공유하는 문제와 장애 이후에 인증 상태를 유지하는 문제는 그대로 남는다.
|
||||
인스턴스 장애와 Replica 간 이동까지 고려한다면 Session과 Authorized Client의 저장 방식을 별도로 설계해야 한다.
|
||||
세션과 Authorized Client를 공유하는 문제, 장애 뒤에 인증 상태를 잇는 문제는 따로 풀어야 한다.
|
||||
인스턴스 장애와 레플리카 간 이동까지 보려면 세션과 Authorized Client의 저장 방식을 따로 설계한다.
|
||||
|
||||
### 4. session은 Redis, token은 암호화한 JDBC에 둔다
|
||||
### 4. 세션은 Redis, 토큰은 암호화해 JDBC에 둔다
|
||||
|
||||
Session과 Authorized Client를 서로 다른 저장소에 보관하는 방법도 있다.
|
||||
요청마다 자주 조회되는 Session은 Redis와 같이 빠르게 접근할 수 있는 저장소에 두고, Access Token과 Refresh Token은 암호화와 장기 보관 정책을 적용하기 쉬운 관계형 DB에 저장할 수 있다.
|
||||
각 데이터의 접근 패턴과 보호 요구사항에 맞춰 저장소를 선택할 수 있다는 장점이 있다.
|
||||
세션과 Authorized Client를 서로 다른 저장소에 담는 방법도 있다.
|
||||
요청마다 자주 조회되는 세션은 Redis처럼 빠르게 접근할 수 있는 저장소에 두고, 액세스 토큰과 리프레시 토큰은 암호화와 장기 보관 정책을 적용하기 쉬운 관계형 DB에 담는 식이다.
|
||||
|
||||
두 저장소의 상태는 함께 관리해야 한다.
|
||||
Session이 만료되었는데 Authorized Client가 남거나, 반대로 Authorized Client가 먼저 제거되어 유효한 Session에서 Token을 찾지 못하는 상황이 발생할 수 있다.
|
||||
따라서 각각의 만료 정책을 어떻게 맞출지 정해야 한다.
|
||||
대신 두 저장소의 상태를 함께 관리해야 한다.
|
||||
세션이 만료됐는데 Authorized Client는 아직 지워지지 않거나, 반대로 Authorized Client가 먼저 지워져서 유효한 세션인데도 토큰을 찾지 못하는 상황이 생길 수 있다.
|
||||
그래서 각각의 만료 정책을 어떻게 맞출지 정해야 한다.
|
||||
|
||||
Logout에서도 Session과 Authorized Client가 서로 다른 저장소에 있으므로 두 상태를 모두 정리해야 한다.
|
||||
한쪽을 제거하는 과정에서 실패했을 때 어떻게 처리할지도 함께 결정해야 한다.
|
||||
로그아웃에서도 세션과 Authorized Client가 서로 다른 저장소에 있으니 두 상태를 모두 지워야 하고, 한쪽을 지우다 실패했을 때 어떻게 할지도 같이 정한다.
|
||||
|
||||
또한 Redis와 관계형 DB를 모두 인증 경로에서 사용하게 되므로 모니터링, 장애 대응, 백업 등 운영해야 하는 저장소도 늘어난다.
|
||||
저장소를 나눠서 얻는 것과 늘어나는 운영 대상을 같이 놓고 정한다.
|
||||
Redis와 관계형 DB를 둘 다 인증 경로에서 쓰게 되므로 모니터링, 장애 대응, 백업까지 운영할 저장소가 늘어난다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
후보마다 같은 입력으로 비교한다.
|
||||
|
||||
1. 인스턴스 두 대에서 로그인 유지와 재시작 복구가 되는지 본다.
|
||||
2. 저장소를 직접 열어 refresh token이 평문으로 남는지 확인한다.
|
||||
3. session TTL과 token 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다.
|
||||
4. logout 뒤 두 store에 잔여 항목이 없는지 확인한다.
|
||||
2. 저장소를 직접 열어 리프레시 토큰이 평문으로 보이는지 확인한다.
|
||||
3. 세션 TTL과 토큰 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다.
|
||||
4. 로그아웃 뒤 두 저장소에 잔여 항목이 없는지 확인한다.
|
||||
5. 저장소를 끊은 상태에서 로그인과 API 호출이 어떤 오류를 내는지 본다.
|
||||
|
||||
암호화 key 교체 절차는 후보를 고른 뒤에 따로 설계한다.
|
||||
암호화 키 교체 절차는 후보를 고른 뒤에 따로 설계한다.
|
||||
|
||||
Reference in New Issue
Block a user