8.9 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이 호스트에도 열려 있어서, 모든 클라이언트가 BFF만 거치도록 네트워크가 강제하고 있지는 않다.
선택지
1. 공유 저장소 — 상태 일관성과 저장소 가용성
HttpSession과 Authorized Client를 외부 공유 저장소에 두면 요청이 다른 인스턴스로 이동해도 같은 로그인 상태와 OAuth 토큰을 조회할 수 있다. 인스턴스 재시작과 라우팅 변경에서 상태를 이어 가는 대신 인증 경로가 그 저장소의 가용성에 의존한다.
이 선택의 핵심은 두 상태의 수명과 보호 방식이다. 세션과 토큰의 만료 시점, 로그아웃 때 지우는 순서, 저장 token 보호가 맞지 않으면 한쪽 상태만 남을 수 있다.
2. session affinity — 라우팅은 고정하지만 node loss는 남는다
Sticky Session은 같은 세션의 요청을 되도록 같은 인스턴스로 보낸다. 기존 메모리 저장 구조를 유지할 수 있다는 장점은 있지만 상태를 다른 인스턴스에 복제하지는 않는다.
고정된 인스턴스가 종료되면 그 메모리에 있던 로그인 세션과 OAuth 토큰도 사라진다. 따라서 이 선택은 정상 라우팅 중의 이동을 줄이는 방법이지, node loss 뒤 상태 복구 방법은 아니다.
3. 브라우저 토큰 — 공유할 서버 상태 자체를 없앤다
SPA(Single Page Application)처럼 브라우저가 OAuth 토큰을 들고 Resource Server를 직접 호출하면 애플리케이션 인스턴스가 공유할 로그인 세션과 OAuth 토큰 저장소가 사라진다. Resource Server는 요청마다 전달된 Access Token을 검증한다.
대신 자격 증명 소유권이 브라우저로 이동한다. 브라우저에 OAuth 토큰을 노출하지 않는 것이 요구사항이면 저장소 문제를 없애더라도 이 선택은 조건과 충돌한다.
4. client-side cookie — 저장소 문제가 아니라 신뢰 경계가 바뀐다
Forward-Auth의 최소 client-side cookie는 공유 저장소나 Sticky Session과 같은 층의 선택지가 아니다. 서버에 application-owned OAuth 상태를 두는 구조를 줄이고 인증 프록시가 cookie를 검증한 결과를 upstream identity로 전달한다.
레플리카는 같은 Cookie Secret을 검증할 수 있어야 하고, upstream은 프록시가 전달한 사용자 정보를 신뢰한다. 그래서 이 구조의 핵심 문제는 세션 저장소 일관성보다 secret 배포·교체, 직접 접근 차단, client-supplied identity header 제거 또는 덮어쓰기다.
다음 검증
인스턴스를 둘로 띄우고 순서대로 확인한다.
- 한쪽에서 로그인한 뒤 다른 인스턴스로 요청을 보내 200이 그대로 나오는지 본다.
- 인스턴스 하나를 재시작하고 같은 세션 쿠키로 로그인 상태가 유지되는지 본다.
- 같은 사용자로 두 브라우저에서 로그인해 Authorized Client 항목이 서로를 덮어쓰는지 본다.
- 한쪽에서 로그아웃한 뒤 다른 쪽 요청이 어떤 응답을 받는지 본다.
- 세션 만료를 토큰 만료보다 짧게 두고, 다시 길게 두고, 각 경우의 응답과 화면을 기록한다.
각 항목의 결과는 어느 엔드포인트와 핸들러가 요청을 받았는지, 다음 요청에 무엇이 입력으로 들어갔는지, 최종 응답이 무엇이었는지까지 적는다.