- 계약 채택 — 독자 질문, 후보 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>
141 lines
9.1 KiB
Markdown
141 lines
9.1 KiB
Markdown
---
|
|
id: 18a5cde2-dd1e-4bff-9f1c-997577ae438f
|
|
kind: QUESTION
|
|
slug: bff-session-authorized-client-store
|
|
title: BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
|
|
topic: oauth-oidc-auth-boundary
|
|
topicName: OAuth/OIDC 인증 경계
|
|
project: KeyCloak Patterns
|
|
status: 게시 중
|
|
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를 어디에 저장할 것인가
|
|
|
|
현재 BFF의 세션과 Authorized Client는 프로세스 메모리에 저장된다. 재시작과 레플리카 이동 뒤에도 로그인 상태를 유지하려면 두 상태를 어디에 저장할지 정해야 한다.
|
|
|
|
BFF(Backend for Frontend)가 서버에 들고 있는 상태는 둘이다. 하나는 브라우저의 로그인 세션이고,
|
|
다른 하나는 Keycloak에서 받은 액세스 토큰과 리프레시 토큰을 담아 두는 Authorized Client다.
|
|
세션은 `session ID`로 찾지만 Authorized Client는 `client registration 이름`과 `principal name`으로 찾는다.
|
|
|
|
찾는 열쇠가 이렇게 다르니 두 상태를 반드시 한 저장소에 담아야 하는 것은 아니다.
|
|
각각의 조회 방식과 운영 요구사항에 맞게 저장 구조를 따로 정할 수 있다.
|
|
|
|
공유 저장소 후보로 Redis를 우선 보고 있지만 아직 고르지 않았다.
|
|
액세스 토큰과 리프레시 토큰을 Redis에 담는다면 토큰을 어떤 방식으로 암호화할지, 세션과 토큰의 만료 시간을 어떻게 맞출지,
|
|
로그아웃할 때 세션과 Authorized Client가 모두 지워지는지를 확인해야 하는데 아직 확인하지 않았다.
|
|
|
|
토큰 보호와 만료 처리, 로그아웃 뒤 상태 정리까지 검증한 다음에 저장 구조를 정한다.
|
|
|
|
## 관계
|
|
|
|
- **서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가**
|
|
이 질문에서 저장소 부분만 떼어 낸 것이다.
|
|
- **Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정**
|
|
세션과 Authorized Client를 찾는 열쇠가 다르다는 사실의 출처다.
|
|
- **BFF 인증 구조 설계 기준**
|
|
이 기준의 저장소 항목은 여기에 답이 나와야 채울 수 있다.
|
|
- **Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가**
|
|
저장소를 공유한 다음에야 레플리카 경쟁을 재현할 수 있다.
|
|
|
|
## 사실
|
|
|
|
- 현재 구성에는 Spring Session도, Redis도, JDBC 저장소도, 토큰을 암호화해 담는 저장소도 없다.
|
|
- HttpSession은 서블릿 컨테이너의 메모리 구현을 쓰기 때문에, 그 프로세스가 종료되면 세션 데이터도 같이 사라진다.
|
|
- OAuth2AuthorizedClientService도 Spring Boot 자동구성이 고르는 메모리 구현이다.
|
|
- 세션은 session ID로 조회하고 Authorized Client는 registration 이름과 principal name으로 조회한다.
|
|
두 저장 구조를 공유 저장소로 옮길 때는 각각 따로 설계해야 한다.
|
|
- Authorized Client 매니저에는 authorization-code와 refresh-token provider가 함께 구성돼 있어서,
|
|
저장소를 공유하면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있다.
|
|
|
|
## 가정
|
|
|
|
- 두 상태를 같은 저장소에 둘 필요는 없다.
|
|
- 저장한 리프레시 토큰을 평문으로 두면 안 된다.
|
|
- 세션 만료와 토큰 만료 중 하나가 먼저 오면 그 순간에 무엇이 일어날지 정해 두어야 한다.
|
|
토큰이 만료됐을 때 사용자의 로그인 상태가 유지되는지가 그런 예다.
|
|
|
|
## 미지수
|
|
|
|
- Redis와 JDBC 중 어느 쪽이 이 상태의 접근 패턴에 맞는가.
|
|
요청마다 읽는 값과 가끔 읽는 값이 섞여 있다.
|
|
- 세션과 Authorized Client를 한 저장소에 둘지 나눌지.
|
|
- 암호화 키를 어디에 두고 어떻게 교체하는가. 교체하는 동안 이전 키로 저장한 값은 어떻게 읽는가.
|
|
- 세션 TTL과 리프레시 토큰 수명 중 어느 쪽을 기준으로 만료를 맞추는가.
|
|
- 로그아웃 한 번으로 세션과 Authorized Client 두 저장소를 어떻게 같이 지우는가.
|
|
- 스티키 세션이 영속 저장소의 대안인가 보완인가.
|
|
|
|
## 제약
|
|
|
|
- Authorized Client를 찾을 때는 session ID를 쓰지 않아서, 세션만 공유해도 같은 사용자의 여러 세션이 같은 토큰을 본다.
|
|
- 현재 테스트에는 저장소 계약이 없다.
|
|
- 모든 화면 요청이 BFF를 지나기 때문에 저장소가 느려지면 화면도 바로 느려진다.
|
|
|
|
## 선택지
|
|
|
|
### 1. 세션과 Authorized Client를 모두 Redis에 둔다
|
|
|
|
Spring Session Redis와 Redis 기반 Authorized Client 저장소를 쓰면 여러 애플리케이션 인스턴스가 같은 세션과 Authorized Client를 조회할 수 있다.
|
|
상태가 특정 인스턴스의 메모리에 묶이지 않으니 서버가 재시작되거나 요청이 다른 레플리카로 전달되는 환경에서도 인증 상태를 이어 쓰기 쉬워진다.
|
|
|
|
세션과 Authorized Client에 설정한 유효 시간에 맞춰 Redis의 TTL로 저장한 상태를 만료시키는 구조도 짤 수 있다.
|
|
다만 어떤 상태를 얼마 동안 유지할지는 애플리케이션의 세션 정책과 OAuth 토큰 수명에 맞춰 따로 정한다.
|
|
|
|
대신 인증 경로가 Redis의 가용성에 매인다.
|
|
Redis에 장애가 났을 때 기존 세션과 Authorized Client를 읽지 못하는 상황을 어떻게 처리할지 정해야 하고, 장애 복구와 데이터 유지 방식도 같이 본다.
|
|
|
|
액세스 토큰과 리프레시 토큰을 Redis에 담는다면 담은 토큰을 어떻게 보호할지도 정해야 한다.
|
|
암호화해서 저장할지, 암호화한다면 키를 어디에 보관하고 어떻게 교체할지까지 저장소 설계에 포함한다.
|
|
|
|
### 2. 세션과 Authorized Client를 모두 JDBC에 둔다
|
|
|
|
JDBC 기반 저장소를 쓰면 이미 운영 중인 관계형 DB에 세션과 Authorized Client를 담을 수 있다.
|
|
|
|
인증 과정에서 세션이나 Authorized Client를 조회할 때마다 DB 접근이 일어나므로, 인증 요청이 기존 관계형 DB의 가용성과 성능에 매이게 된다.
|
|
요청량이 늘었을 때 세션 조회가 지연되지 않는지, 인증 조회와 기존 애플리케이션 쿼리가 서로 영향을 주지 않는지 확인해야 한다.
|
|
|
|
만료된 세션과 Authorized Client 데이터가 계속 쌓이지 않도록 정리하는 방법과 주기도 정한다.
|
|
|
|
### 3. 세션만 공유하고 스티키 세션을 쓴다
|
|
|
|
세션 어피니티를 쓰면 같은 사용자의 요청을 되도록 같은 애플리케이션 인스턴스로 보낼 수 있어서 기존 구조를 덜 건드린다.
|
|
|
|
하지만 이것만으로 인증 상태가 여러 인스턴스에 공유되지는 않는다.
|
|
Authorized Client가 여전히 특정 애플리케이션 인스턴스의 메모리에 있다면, 요청이 다른 인스턴스로 전달되거나 그 인스턴스가 종료됐을 때 기존 토큰 정보를 찾지 못한다.
|
|
|
|
세션과 Authorized Client를 공유하는 문제, 장애 뒤에 인증 상태를 잇는 문제는 따로 풀어야 한다.
|
|
인스턴스 장애와 레플리카 간 이동까지 보려면 세션과 Authorized Client의 저장 방식을 따로 설계한다.
|
|
|
|
### 4. 세션은 Redis, 토큰은 암호화해 JDBC에 둔다
|
|
|
|
세션과 Authorized Client를 서로 다른 저장소에 담는 방법도 있다.
|
|
요청마다 자주 조회되는 세션은 Redis처럼 빠르게 접근할 수 있는 저장소에 두고, 액세스 토큰과 리프레시 토큰은 암호화와 장기 보관 정책을 적용하기 쉬운 관계형 DB에 담는 식이다.
|
|
|
|
대신 두 저장소의 상태를 함께 관리해야 한다.
|
|
세션이 만료됐는데 Authorized Client는 아직 지워지지 않거나, 반대로 Authorized Client가 먼저 지워져서 유효한 세션인데도 토큰을 찾지 못하는 상황이 생길 수 있다.
|
|
그래서 각각의 만료 정책을 어떻게 맞출지 정해야 한다.
|
|
|
|
로그아웃에서도 세션과 Authorized Client가 서로 다른 저장소에 있으니 두 상태를 모두 지워야 하고, 한쪽을 지우다 실패했을 때 어떻게 할지도 같이 정한다.
|
|
|
|
Redis와 관계형 DB를 둘 다 인증 경로에서 쓰게 되므로 모니터링, 장애 대응, 백업까지 운영할 저장소가 늘어난다.
|
|
|
|
## 다음 검증
|
|
|
|
후보마다 같은 입력으로 비교한다.
|
|
|
|
1. 인스턴스 두 대에서 로그인 유지와 재시작 복구가 되는지 본다.
|
|
2. 저장소를 직접 열어 리프레시 토큰이 평문으로 보이는지 확인한다.
|
|
3. 세션 TTL과 토큰 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다.
|
|
4. 로그아웃 뒤 두 저장소에 잔여 항목이 없는지 확인한다.
|
|
5. 저장소를 끊은 상태에서 로그인과 API 호출이 어떤 오류를 내는지 본다.
|
|
|
|
암호화 키 교체 절차는 후보를 고른 뒤에 따로 설계한다.
|