- 계약 채택 — 독자 질문, 후보 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>
9.8 KiB
id, kind, slug, title, topic, topicName, project, status, version, questionStatus, studio, public, sourceRevision, source
| id | kind | slug | title | topic | topicName | project | status | version | questionStatus | studio | public | sourceRevision | source | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| c72656b5-842d-45d9-b5f6-82b66b09d0b9 | QUESTION | server-session-pattern-multi-instance | 서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가 | oauth-oidc-auth-boundary | OAuth/OIDC 인증 경계 | KeyCloak Patterns | 게시 중 | 39 | OPEN | https://hyeonworks.com/studio/documents/c72656b5-842d-45d9-b5f6-82b66b09d0b9/edit | https://hyeonworks.com/questions/server-session-pattern-multi-instance | keycloak-patterns-lab@2026-08 |
|
서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가
Mediator와 BFF(Backend for Frontend)는 브라우저의 로그인 세션과 OAuth 토큰을 서로 다른 저장소에 둔다. 지금 구현은 두 저장소를 모두 애플리케이션 서버의 메모리에 두기 때문에, 서버 프로세스가 끝나면 담아 둔 상태도 같이 사라진다.
운영에서 서버를 여러 인스턴스로 띄우려면 여기에 설계를 더해야 한다. 사용자의 요청이 로그인할 때와 다른 인스턴스로 가도 세션과 토큰을 찾을 수 있어야 하고, 서버를 재시작한 뒤에도 로그인 상태를 이어 갈지 정해야 한다. 로그아웃할 때 여러 인스턴스에 흩어져 있는 세션과 토큰을 어떻게 함께 지울지도 정해야 한다.
관계
- Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정 두 상태가 모두 인스턴스 하나의 메모리에 있다는 사실을 이 기록에서 가져왔다.
- Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출 이 패턴도 세션과 토큰을 같은 방식으로 저장한다.
- BFF 인증 구조 설계 기준 이 질문에 답하면 그 기준에서 비어 있는 항목이 채워진다.
- BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가 저장소 후보를 견주는 일만 따로 떼어 낸 질문이다.
사실
- Mediator와 BFF는 로그인 상태와 OAuth 토큰을 서로 다른 저장소에 둔다. 로그인 상태는 HttpSession에 담고 세션 ID로 찾는다. OAuth 토큰은 OAuth2AuthorizedClientService에 담고, client registration 이름과 principal name으로 찾는다.
- 세션과 OAuth 토큰을 담을 저장소를 따로 설정하지 않아서, Spring Boot 자동 구성이 고르는 메모리 기반 기본 구현이 쓰인다. 다만 저장소를 직접 만드는 Bean이 코드에 없으므로, 어떤 구현체가 실제로 올라오는지는 자동 구성 결과까지 열어 봐야 정확히 알 수 있다.
- Spring Session이나 Redis, JDBC 기반 Token Store 같은 외부 저장소를 쓰지 않기 때문에, 로그인 세션과 OAuth 토큰은 모두 그 애플리케이션 인스턴스의 메모리에 있다. 인스턴스가 종료되거나 재시작되면 그 인스턴스가 들고 있던 로그인 세션과 OAuth 토큰도 같이 사라진다.
- Authorized Client는 세션별로 나뉘지 않는다. 조회 기준에 세션 ID가 없다 보니 같은 사용자가 여러 브라우저에서 로그인하면 모두 같은 Authorized Client 하나를 쓴다.
- OAuth2-Proxy 구조는 로그인 상태를 서버 저장소에 두지 않고, 필요한 최소한의 정보만 브라우저의 세션 쿠키에 담는다. 지금 설정에서 이 쿠키의 유효 시간은 1시간이다.
- 지금 테스트에는 서버를 재시작하거나 요청이 다른 인스턴스로 갔을 때 로그인 상태와 OAuth 토큰을 그대로 쓸 수 있는지 확인하는 항목이 없다. 그래서 세션이나 토큰의 저장 방식을 바꿔도 기존 동작이 깨지지 않는지는 따로 확인해야 한다.
가정
- 운영에서는 인스턴스가 둘 이상이다.
- 재시작과 배포가 로그인 상태를 끊어서는 안 된다. 지금 구조에서는 끊긴다.
- 같은 사용자가 여러 브라우저에서 로그인해도, 한쪽 세션이 다른 쪽의 토큰 항목을 덮어써서는 안 된다.
미지수
- 재시작한 뒤에 로그인이 유지되는가. 지금은 유지되지 않는다는 것까지만 확인했고, 무엇을 바꿀지는 아직 정하지 않았다.
- 요청이 다른 인스턴스로 가도 같은 세션을 찾는가.
- 같은 사용자의 여러 세션이 Authorized Client 항목 하나를 함께 쓰는가, 아니면 서로 덮어쓰는가. 한쪽에서 로그아웃하면 다른 쪽 로그인도 끊기는가.
- 저장한 Refresh Token이 암호화되는가. 저장소를 여는 사람이 그 값을 그대로 읽는가.
- 로그아웃할 때 HttpSession과 Authorized Client를 모두 지우는가. 한쪽만 지웠을 때 다음 요청이나 재로그인에서 어떤 상태가 살아나는가.
- 세션 만료와 토큰 만료가 어긋나면 무엇이 먼저 실패하고 사용자 화면에는 무엇이 보이는가.
- OAuth2-Proxy 구조의 레플리카들은 같은 Cookie Secret을 어떻게 나눠 갖고 어떻게 바꾸는가. 바꾸는 동안 이미 로그인해 있던 사람은 어떻게 되는가.
제약
- 지금은 인스턴스 하나로만 실행하고 있어서 레플리카 사이의 세션 조회나 장애 조치(failover) 동작은 아직 만들지 않았다.
- Authorized Client는 세션 ID로 저장하지도 조회하지도 않는다. 그래서 여러 인스턴스가 같은 세션을 쓰도록 세션 저장소만 공유 저장소로 바꿔서는 부족하다. 로그인 세션을 여러 인스턴스가 나눠 쓰는 방법과, OAuth 토큰이 들어 있는 Authorized Client를 어디에 어떻게 저장할지는 각각 따로 설계해야 한다.
- Resource Server의 8081이 host에도 열려 있어서, 모든 클라이언트가 BFF만 거치도록 네트워크가 강제하고 있지는 않다.
선택지
1. 공유 저장소를 사용한다
HttpSession과 Authorized Client를 모두 바깥의 공유 저장소에 두면 여러 애플리케이션 인스턴스가 같은 로그인 세션과 OAuth 토큰을 조회할 수 있다. 요청이 다른 인스턴스로 가거나 인스턴스 하나가 재시작해도 기존 로그인 상태를 그대로 쓸 수 있다.
저장소에 장애가 나면 인증 요청을 어떻게 처리할지 정해야 하고, 세션과 토큰을 어떤 형식으로 저장할지와 저장한 토큰을 어떻게 보호할지도 정해야 한다. 세션은 아직 유효한데 토큰은 이미 만료된 것 같은 어긋남이 생기지 않도록, 두 상태의 만료 시간과 지우는 시점도 함께 설계해야 한다.
2. session affinity로 같은 인스턴스에 붙인다
Sticky Session은 같은 세션에서 온 요청을 되도록 같은 애플리케이션 인스턴스로 보내는 방식이다. 지금의 메모리 기반 세션·토큰 저장을 그대로 두어도 되므로 애플리케이션 코드는 거의 손대지 않고, 공유 저장소도 따로 두지 않는다.
다만 그 인스턴스가 종료되면 그 메모리에 있던 로그인 세션과 OAuth 토큰도 함께 사라진다. 배포나 오토스케일링으로 인스턴스가 자주 바뀌는 환경이라면 Sticky Session만으로 로그인 상태를 지키기 어렵고, 인스턴스가 사라졌을 때 상태를 어떻게 되살릴지 따로 설계해야 한다.
3. 브라우저가 token을 들고 API를 직접 부른다
서버에 로그인 세션이나 OAuth 토큰을 두지 않는 구조로 바꾸면 여러 인스턴스가 나눠 가질 상태 자체가 없어서, 공유 저장소도 session affinity도 필요하지 않다. Resource Server는 요청마다 실려 온 Access Token을 검증해서 처리한다.
SPA(Single Page Application)처럼 브라우저가 OAuth 토큰을 직접 들고 API를 부르는 구조가 여기에 해당한다. 다만 브라우저에 OAuth 토큰을 노출하지 않는 것이 조건이라면 이 선택지는 뺀다.
4. 층이 다른 선택지 — 최소 정보만 담은 client-side cookie
이 방식은 세션이나 토큰 저장소를 다른 저장소로 바꾸는 것이 아니다. 서버에 인증 상태를 두는 구조 자체를 없애고, 필요한 인증 상태만 쿠키에 담아 보낸다. 그래서 공유 저장소나 Sticky Session처럼 서버 상태를 어떻게 지킬지 정하는 방법과 나란히 놓고 견주기 어렵다.
Forward-Auth는 실제 요청을 넘기기 전에 별도의 인증 엔드포인트에 허용 여부를 먼저 묻는 방식이다. 이 구조로 옮기면 애플리케이션이 OAuth 토큰을 서버에 직접 저장하고 관리하지 않아도 된다. 대신 여러 인스턴스가 같은 인증 쿠키를 풀 수 있도록 Cookie Secret을 나눠 가져야 한다. 그리고 인증 프록시가 넘겨 주는 사용자 정보를 애플리케이션이 믿으므로, 바깥 요청이 그 헤더를 위조하지 못하게 네트워크 접근 경로와 전달 헤더를 함께 관리해야 한다.
다음 검증
인스턴스를 둘로 띄우고 순서대로 확인한다.
- 한쪽에서 로그인한 뒤 다른 인스턴스로 요청을 보내 200이 그대로 나오는지 본다.
- 인스턴스 하나를 재시작하고 같은 세션 쿠키로 로그인 상태가 유지되는지 본다.
- 같은 사용자로 두 브라우저에서 로그인해 Authorized Client 항목이 서로를 덮어쓰는지 본다.
- 한쪽에서 로그아웃한 뒤 다른 쪽 요청이 어떤 응답을 받는지 본다.
- 세션 만료를 토큰 만료보다 짧게 두고, 다시 길게 두고, 각 경우의 응답과 화면을 기록한다.
각 항목의 결과는 어느 엔드포인트와 핸들러가 요청을 받았는지, 다음 요청에 무엇이 입력으로 들어갔는지, 최종 응답이 무엇이었는지까지 적는다.