refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
@@ -46,7 +46,7 @@ source:
두 저장 구조를 공유 저장소로 옮길 때는 각각 따로 설계해야 한다.
- Authorized Client 매니저에는 authorization-code와 refresh-token provider가 함께 구성돼 있어서,
저장소를 공유하면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있다.
- 여기까지는 한 대에서 실행한 학습 환경에서 코드와 테스트로 확인한 것이다.
- 현재 확인 범위는 한 대에서 실행한 학습 환경 코드와 테스트다.
여러 인스턴스가 세션과 Authorized Client를 공유하는지는 실행해 확인하지 못했다.
## 가정
@@ -93,7 +93,7 @@ source:
이러면 엣지는 애플리케이션의 역할과 테넌트, 권한 정책을 알 필요가 없다. BFF가 사용자와 권한을 조회해 인가를 판단하고, 화면에 필요한 여러 Resource Server의 API를 불러 결과를 합칠 수 있다.
대신 BFF를 넣으면 서버가 다시 인증 상태를 들고 있어야 한다. 브라우저와 BFF 사이의 애플리케이션 세션을 보호해야 한다. 쿠키 기반 세션을 쓴다면 쿠키가 요청마다 자동으로 붙으니, 값을 바꾸는 요청에는 사용자가 의도한 것인지 확인하는 CSRF(Cross-Site Request Forgery) 검증도 있어야 한다. BFF를 여러 대로 늘려 인증 상태를 유지하려면 세션과 authorized client를 어떻게 공유할지 정하고, 공유 저장소의 장애와 만료 처리까지 운영해야 한다.
대신 BFF를 넣으면 서버가 다시 인증 상태를 들고 있어야 한다. 브라우저와 BFF 사이의 애플리케이션 세션을 보호해야 한다. 쿠키 기반 세션을 쓴다면 쿠키가 요청마다 자동으로 붙으니, 값을 바꾸는 요청에는 별도 anti-CSRF token을 요구해 cross-site forged request를 구분하는 CSRF(Cross-Site Request Forgery) 검증도 있어야 한다. BFF를 여러 대로 늘려 인증 상태를 유지하려면 세션과 authorized client를 어떻게 공유할지 정하고, 공유 저장소의 장애와 만료 처리까지 운영해야 한다.
이 구조를 골랐다가 다시 엣지 쪽으로 되돌린다면 BFF가 맡던 사용자별 인가를 업스트림이나 별도 정책 서비스로 다시 옮겨야 한다.
@@ -65,29 +65,29 @@ Mediator와 BFF(Backend for Frontend)는 브라우저의 로그인 세션과 OAu
## 선택지
### 1. 공유 저장소를 사용한다
### 1. 공유 저장소 — 상태 일관성과 저장소 가용성
HttpSession과 Authorized Client를 모두 바깥의 공유 저장소에 두면 여러 애플리케이션 인스턴스가 같은 로그인 세션과 OAuth 토큰을 조회할 수 있다. 요청이 다른 인스턴스로 가거나 인스턴스 하나가 재시작해도 기존 로그인 상태를 그대로 쓸 수 있다.
HttpSession과 Authorized Client를 외부 공유 저장소에 두면 요청이 다른 인스턴스로 이동해도 같은 로그인 상태와 OAuth 토큰을 조회할 수 있다. 인스턴스 재시작과 라우팅 변경에서 상태를 이어 가는 대신 인증 경로가 그 저장소의 가용성에 의존한다.
저장소에 장애가 나면 인증 요청을 어떻게 처리할지 정해야 하고, 세션과 토큰을 어떤 형식으로 저장할지와 저장한 토큰을 어떻게 보호할지도 정해야 한다. 세션은 아직 유효한데 토큰은 이미 만료된 것 같은 어긋남이 생기지 않도록, 두 상태의 만료 시간과 지우는 시점도 함께 설계해야 한다.
이 선택의 핵심은 두 상태의 수명과 보호 방식이다. 세션과 토큰의 만료 시점, 로그아웃 때 지우는 순서, 저장 token 보호가 맞지 않으면 한쪽 상태만 남을 수 있다.
### 2. session affinity로 같은 인스턴스에 붙인
### 2. session affinity — 라우팅은 고정하지만 node loss는 남는
Sticky Session은 같은 세션에서 온 요청을 되도록 같은 애플리케이션 인스턴스로 보내는 방식이다. 지금의 메모리 기반 세션·토큰 저장을 그대로 두어도 되므로 애플리케이션 코드는 거의 손대지 않고, 공유 저장소도 따로 두지 않는다.
Sticky Session은 같은 세션 요청을 되도록 같은 인스턴스로 보낸다. 기존 메모리 저장 구조를 유지할 수 있다는 장점은 있지만 상태를 다른 인스턴스에 복제하지는 않는다.
다만 그 인스턴스가 종료되면 그 메모리에 있던 로그인 세션과 OAuth 토큰도 함께 사라진다. 배포나 오토스케일링으로 인스턴스가 자주 바뀌는 환경이라면 Sticky Session만으로 로그인 상태를 지키기 어렵고, 인스턴스가 사라졌을 때 상태를 어떻게 되살릴지 따로 설계해야 한다.
고정된 인스턴스가 종료되면 그 메모리에 있던 로그인 세션과 OAuth 토큰도 사라진다. 따라서 이 선택은 정상 라우팅 중의 이동을 줄이는 방법이지, node loss 뒤 상태 복구 방법은 아니다.
### 3. 브라우저 토큰을 들고 API를 직접 부른
### 3. 브라우저 토큰 — 공유할 서버 상태 자체를 없앤
서버에 로그인 세션이나 OAuth 토큰을 두지 않는 구조로 바꾸면 여러 인스턴스가 나눠 가질 상태 자체가 없어서, 공유 저장소도 session affinity도 필요하지 않다. Resource Server는 요청마다 실려 온 Access Token을 검증해서 처리한다.
SPA(Single Page Application)처럼 브라우저가 OAuth 토큰을 들고 Resource Server를 직접 호출하면 애플리케이션 인스턴스가 공유할 로그인 세션과 OAuth 토큰 저장소가 사라진다. Resource Server는 요청마다 전달된 Access Token을 검증한다.
SPA(Single Page Application)처럼 브라우저가 OAuth 토큰을 직접 들고 API를 부르는 구조가 여기에 해당한다. 다만 브라우저에 OAuth 토큰을 노출하지 않는 것이 조건이라면 이 선택지는 뺀다.
대신 자격 증명 소유권이 브라우저로 이동한다. 브라우저에 OAuth 토큰을 노출하지 않는 것이 요구사항이면 저장소 문제를 없애더라도 이 선택은 조건과 충돌한다.
### 4. 층이 다른 선택지 — 최소 정보만 담은 client-side cookie
### 4. client-side cookie — 저장소 문제가 아니라 신뢰 경계가 바뀐다
이 방식은 세션이나 토큰 저장소를 다른 저장소로 바꾸는 것이 아니다. 서버에 인증 상태를 두는 구조 자체를 없애고, 필요한 인증 상태만 쿠키에 담아 보낸다. 그래서 공유 저장소나 Sticky Session처럼 서버 상태를 어떻게 지킬지 정하는 방법과 나란히 놓고 견주기 어렵다.
Forward-Auth의 최소 client-side cookie는 공유 저장소나 Sticky Session과 같은 층의 선택지가 아니다. 서버에 application-owned OAuth 상태를 두는 구조를 줄이고 인증 프록시가 cookie를 검증한 결과를 upstream identity로 전달한다.
Forward-Auth는 실제 요청을 넘기기 전에 별도의 인증 엔드포인트에 허용 여부를 먼저 묻는 방식이다. 이 구조로 옮기면 애플리케이션이 OAuth 토큰을 서버에 직접 저장하고 관리하지 않아도 된다. 대신 여러 인스턴스가 같은 인증 쿠키를 풀 수 있도록 Cookie Secret을 나눠 가져야 한다. 그리고 인증 프록시가 넘겨 주는 사용자 정보를 애플리케이션이 믿으므로, 바깥 요청이 그 헤더를 위조하지 못하게 네트워크 접근 경로와 전달 헤더를 함께 관리해야 한다.
레플리카는 같은 Cookie Secret을 검증할 수 있어야 하고, upstream은 프록시가 전달한 사용자 정보를 신뢰한다. 그래서 이 구조의 핵심 문제는 세션 저장소 일관성보다 secret 배포·교체, 직접 접근 차단, client-supplied identity header 제거 또는 덮어쓰기다.
## 다음 검증
@@ -19,7 +19,7 @@ source:
# Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가
realm은 refresh token rotation과 재사용 허용 0회를 쓴다. 한 번 갱신하면 이전 refresh token이 바로
realm은 refresh token rotation과 최대 재사용 횟수 0회를 쓴다. `refreshTokenMaxReuse`는 시간 창이 아니라 동일 refresh token의 허용 재사용 횟수를 나타낸다. 한 번 갱신하면 이전 refresh token이 바로
무효가 되기 때문에, 두 replica가 같은 refresh token으로 동시에 갱신하면 두 번째 사용이 거부될 수 있다.
실제 Keycloak 응답과 session에 미치는 영향은 아직 재현하지 않았다.
@@ -28,7 +28,7 @@ realm은 refresh token rotation과 재사용 허용 0회를 쓴다. 한 번 갱
- **BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가**
이 질문에 답하기 전에 저장소부터 정해야 한다.
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
rotation과 재사용 0회를 쓰는 구성이 여기서 나왔다.
rotation과 최대 재사용 횟수 0회를 쓰는 구성이 여기서 나왔다.
- **서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가**
인스턴스를 여러 대 띄워야 이 경쟁이 생긴다.
- **BFF 인증 구조 설계 기준**
@@ -36,12 +36,13 @@ realm은 refresh token rotation과 재사용 허용 0회를 쓴다. 한 번 갱
## 사실
- realm에는 refresh token rotation과 재사용 허용 0회가 설정되어 있다.
- realm에는 refresh token rotation과 최대 재사용 횟수 0회가 설정되어 있다.
- 커밋된 테스트는 새 refresh token이 발급되는지, 이전 token이 거부되는지, revocation 뒤 refresh가
실패하는지를 확인한다. 다만 이 셋은 refresh token을 직접 써서 받은 결과라, 애플리케이션이 스스로
갱신할 때도 같은 결과가 나온다고 보지 않는다.
- authorized client manager에는 refresh-token provider가 구성되어 있어, access token이 만료되면
refresh를 시도할 수 있다.
- `refreshTokenMaxReuse`는 재사용 횟수 설정이고 refresh token lifespan은 별도의 시간 설정이다. 이 문서에서는 둘을 같은 허용 시간으로 취급하지 않는다.
- access token이 만료될 때까지 기다려 실제로 갱신이 성공하고 새 token이 저장되는지까지는 확인하지 않았다.
- 현재 authorized client 저장소는 프로세스 안에만 있어서(process-local) replica끼리 같은 refresh token
상태를 공유하지 않는다. 그래서 이번 단일 인스턴스 검증에서는 동시 refresh 경쟁을 재현하지 않았다.
@@ -56,7 +57,7 @@ realm은 refresh token rotation과 재사용 허용 0회를 쓴다. 한 번 갱
## 미지수
- 같은 refresh token으로 두 replica가 동시에 갱신하면 각 replica가 어떻게 동작하는가.
- 재사용 허용 0회에서 두 번째 사용이 거부되면 사용자 화면에 무엇이 보이는가.
- 최대 재사용 횟수 0회에서 두 번째 사용이 거부되면 사용자 화면에 무엇이 보이는가.
로그인 만료로 보이는가, 일시적 오류로 보이는가.
- 저장소에서 새 token을 다시 읽어 재시도하면 성공하는가, 아니면 재인증까지 해야 하는가.
- 갱신을 한 곳에서만 할 것인가, 각자 하게 두고 실패는 재시도로 처리할 것인가.
@@ -65,7 +66,7 @@ realm은 refresh token rotation과 재사용 허용 0회를 쓴다. 한 번 갱
## 제약
- rotation과 재사용 0회는 이미 realm에 설정했다. 이 전제는 바꾸지 않고 답한다.
- rotation과 최대 재사용 횟수 0회는 이미 realm에 설정했다. 이 전제는 바꾸지 않고 답한다.
- 이미 발급된 access token은 만료 전까지 쓸 수 있으므로 refresh 실패가 곧바로 드러나지 않을 수 있다.
재현 테스트는 access token 만료 직후에 맞춰 실행한다.
- 이 경쟁은 저장소를 공유한 뒤에야 재현되므로 저장소를 정한 다음에 이어서 푼다.
@@ -96,11 +97,9 @@ refresh를 전담하는 구성요소 하나만 refresh token을 쓰고, 다른 r
이 구성요소가 멈추면 access token이 만료된 뒤에 갱신할 주체가 없어진다. 그래서 이 구성요소를 어떻게
살려 두고 어떻게 복구할지를 먼저 정해야 한다.
### 4. 제약상 제외 — 재사용 허용을 늘린다
### 4. 제약상 제외 — 최대 재사용 횟수를 늘린다
재사용을 짧게 허용하면 두 번째 사용이 거부되지 않고 코드도 고칠 필요가 없다.
다만 rotation과 재사용 0회는 이미 realm에 설정했고, 허용한 시간 안에는 훔친 refresh token이 들어와도
막지 못한다.
최대 재사용 횟수를 늘리면 동일 refresh token의 추가 사용을 일정 횟수 받아들이도록 구성할 수 있다. 그만큼 탈취된 refresh token의 replay를 허용할 여지도 커진다. 현재 실험은 rotation과 최대 재사용 횟수 0회를 전제로 하므로 이 선택지는 제외한다.
## 다음 검증
@@ -113,4 +112,4 @@ refresh를 전담하는 구성요소 하나만 refresh token을 쓰고, 다른 r
5. lock을 넣은 구성과 안 넣은 구성을 같은 입력으로 비교해 실패율과 지연을 잰다.
실패가 사용자에게 노출되면 lock을 고르고, 노출되지 않으면 재시도로 둔다.
rotation과 재사용 0회를 바꾸는 선택지는 지금은 제외로 두고 나중에 다시 본다.
rotation과 최대 재사용 횟수 0회를 바꾸는 선택지는 지금은 제외로 두고 나중에 다시 본다. 정확한 Keycloak runtime의 동시 refresh 결과는 source repository와 실행 환경을 다시 사용할 수 있을 때 realm 값과 함께 별도 evidence로 확인한다.