- generic [ref=f10e3]: - link "본문으로 건너뛰기" [ref=f10e4] [cursor=pointer]: - /url: "#main-content" - banner [ref=f10e5]: - generic [ref=f10e6]: - link "TechLog Studio" [ref=f10e7] [cursor=pointer]: - /url: /studio - text: TechLog - generic [ref=f10e8]: Studio - navigation "Studio 주 탐색" [ref=f10e10]: - link "작업본" [ref=f10e11] [cursor=pointer]: - /url: /studio/documents - link "게시 기록" [ref=f10e12] [cursor=pointer]: - /url: /studio/publications - link "새 문서" [ref=f10e13] [cursor=pointer]: - /url: /studio/documents/new - link "주제·프로젝트" [ref=f10e14] [cursor=pointer]: - /url: /studio/taxonomy - link "릴리즈" [ref=f10e15] [cursor=pointer]: - /url: /studio/releases - link "공개 사이트 보기" [ref=f10e16] [cursor=pointer]: - /url: / - button "로그아웃" [ref=f10e17] - main [ref=f10e18]: - generic [ref=f10e19]: - generic [ref=f10e20]: - region [ref=f10e21]: - generic [ref=f10e22]: - paragraph [ref=f10e23]: QUESTION · VERSION 11 - heading "문서 편집" [level=1] [ref=f10e24] - paragraph [ref=f10e25]: 서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가 - region [ref=f10e26]: - generic [ref=f10e27]: - paragraph [ref=f10e28]: DOCUMENT - heading "기본 정보" [level=2] [ref=f10e29] - generic [ref=f10e30]: - generic [ref=f10e31]: - generic [ref=f10e32]: 제목 - textbox "제목" [ref=f10e33]: 서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가 - generic [ref=f10e34]: - generic [ref=f10e35]: slug - textbox "slug" [ref=f10e36]: - /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈) - text: server-session-pattern-multi-instance - generic [ref=f10e37]: - generic [ref=f10e38]: 요약 - textbox "요약" [ref=f10e39]: Mediator와 BFF는 로그인 상태와 token 상태를 열쇠가 다른 두 저장소에 나눠 두게 되는데 지금은 두 상태가 다 process 안에 있다. 인스턴스가 둘 이상인 운영에서 재시작과 이동, logout이 어떻게 동작해야 하는지 아직 정하지 않았다. - generic [ref=f10e40]: - generic [ref=f10e41]: Topic - combobox "Topic" [ref=f10e42]: - option "선택하지 않음" - option "OAuth/OIDC 인증 경계" [selected] - generic [ref=f10e43]: - generic [ref=f10e44]: Project - combobox "Project" [ref=f10e45]: - option "미지정" - option "Backend Clean Architecture" - option "KeyCloak Patterns" [selected] - option "Liner N + 1문제" - group "관계" [ref=f10e46]: - generic [ref=f10e48]: - generic [ref=f10e49]: - generic [ref=f10e50]: 관계 1 대상 - combobox "관계 1 대상" [ref=f10e51]: - option "대상 선택" - option "인증 구조를 보안 성숙도 단계로 취급하지 않는다" - option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" - option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" - option "BFF 인증 구조 설계 기준" [disabled] - option "BFF가 OAuth Token을 관리하는 조건" - option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled] - option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [selected] - option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가" - option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" - option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" - option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다" - option "외부 IdP Federation과 Application 인증 경계" - option "OAuth/OIDC 인증 패턴 선택 기준" - option "OAuth Token과 Application Session을 구분하는 기준" - option "Public Client와 Confidential Client 구분 기준" - option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled] - option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" - option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" - generic [ref=f10e52]: - generic [ref=f10e53]: 관계 1 이유 - textbox "관계 1 이유" [ref=f10e54]: 두 상태가 모두 process-local memory에 있다는 사실의 출처다. - generic [ref=f10e55]: - button "위로" [disabled] [ref=f10e56] - button "아래로" [ref=f10e57] - button "삭제" [ref=f10e58] - generic [ref=f10e59]: - generic [ref=f10e60]: - generic [ref=f10e61]: 관계 2 대상 - combobox "관계 2 대상" [ref=f10e62]: - option "대상 선택" - option "인증 구조를 보안 성숙도 단계로 취급하지 않는다" - option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" - option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" - option "BFF 인증 구조 설계 기준" [disabled] - option "BFF가 OAuth Token을 관리하는 조건" - option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled] - option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled] - option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가" - option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" - option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" - option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다" - option "외부 IdP Federation과 Application 인증 경계" - option "OAuth/OIDC 인증 패턴 선택 기준" - option "OAuth Token과 Application Session을 구분하는 기준" - option "Public Client와 Confidential Client 구분 기준" - option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [selected] - option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" - option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" - generic [ref=f10e63]: - generic [ref=f10e64]: 관계 2 이유 - textbox "관계 2 이유" [ref=f10e65]: 같은 저장소 구성을 쓰는 다른 패턴이다. - generic [ref=f10e66]: - button "위로" [ref=f10e67] - button "아래로" [ref=f10e68] - button "삭제" [ref=f10e69] - generic [ref=f10e70]: - generic [ref=f10e71]: - generic [ref=f10e72]: 관계 3 대상 - combobox "관계 3 대상" [ref=f10e73]: - option "대상 선택" - option "인증 구조를 보안 성숙도 단계로 취급하지 않는다" - option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" - option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" - option "BFF 인증 구조 설계 기준" [selected] - option "BFF가 OAuth Token을 관리하는 조건" - option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled] - option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled] - option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가" - option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" - option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" - option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다" - option "외부 IdP Federation과 Application 인증 경계" - option "OAuth/OIDC 인증 패턴 선택 기준" - option "OAuth Token과 Application Session을 구분하는 기준" - option "Public Client와 Confidential Client 구분 기준" - option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled] - option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" - option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" - generic [ref=f10e74]: - generic [ref=f10e75]: 관계 3 이유 - textbox "관계 3 이유" [ref=f10e76]: 이 질문의 답이 이 기준의 빈 항목을 채운다. - generic [ref=f10e77]: - button "위로" [ref=f10e78] - button "아래로" [ref=f10e79] - button "삭제" [ref=f10e80] - generic [ref=f10e81]: - generic [ref=f10e82]: - generic [ref=f10e83]: 관계 4 대상 - combobox "관계 4 대상" [ref=f10e84]: - option "대상 선택" - option "인증 구조를 보안 성숙도 단계로 취급하지 않는다" - option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" - option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" - option "BFF 인증 구조 설계 기준" [disabled] - option "BFF가 OAuth Token을 관리하는 조건" - option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [selected] - option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [disabled] - option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가" - option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" - option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" - option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다" - option "외부 IdP Federation과 Application 인증 경계" - option "OAuth/OIDC 인증 패턴 선택 기준" - option "OAuth Token과 Application Session을 구분하는 기준" - option "Public Client와 Confidential Client 구분 기준" - option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled] - option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" - option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" - generic [ref=f10e85]: - generic [ref=f10e86]: 관계 4 이유 - textbox "관계 4 이유" [ref=f10e87]: 저장소 후보 비교로 독립시킨 질문이다. - generic [ref=f10e88]: - button "위로" [ref=f10e89] - button "아래로" [disabled] [ref=f10e90] - button "삭제" [ref=f10e91] - button "관계 추가" [ref=f10e92] - region [ref=f10e93]: - generic [ref=f10e94]: - paragraph [ref=f10e95]: QUESTION - heading "판단과 다음 검증" [level=2] [ref=f10e96] - generic [ref=f10e97]: - generic [ref=f10e98]: 질문 상태 - combobox "질문 상태" [ref=f10e99]: - option "아직 정하지 않음" - option "OPEN" [selected] - option "RESOLVED" - group "사실" [ref=f10e100]: - generic [ref=f10e102]: - generic [ref=f10e103]: - generic [ref=f10e104]: 사실 1 - textbox "사실 1" [ref=f10e105]: Mediator와 BFF는 로그인 상태를 HttpSession에 두고 token은 OAuth2AuthorizedClientService에 두게 되는데, 두 저장소는 열쇠가 다르다. session은 session ID로 찾고 authorized client는 registration 이름과 principal name으로 찾는다. - generic [ref=f10e106]: - button "위로" [disabled] [ref=f10e107] - button "아래로" [ref=f10e108] - button "삭제" [ref=f10e109] - generic [ref=f10e110]: - generic [ref=f10e111]: - generic [ref=f10e112]: 사실 2 - textbox "사실 2" [ref=f10e113]: 현재 두 저장소는 Spring Boot 자동구성이 선택한 in-memory 구현을 사용한다. 코드에서 store bean을 직접 선언하지 않았기 때문에 실제 구현은 자동구성 결과를 함께 확인해야 한다. - generic [ref=f10e114]: - button "위로" [ref=f10e115] - button "아래로" [ref=f10e116] - button "삭제" [ref=f10e117] - generic [ref=f10e118]: - generic [ref=f10e119]: - generic [ref=f10e120]: 사실 3 - textbox "사실 3" [ref=f10e121]: Spring Session과 Redis, JDBC token store 의존성이 없어서 두 상태가 모두 process 안에 있다. 그 instance가 종료되면 그 instance가 들고 있던 session과 authorized client는 사라진다. - generic [ref=f10e122]: - button "위로" [ref=f10e123] - button "아래로" [ref=f10e124] - button "삭제" [ref=f10e125] - generic [ref=f10e126]: - generic [ref=f10e127]: - generic [ref=f10e128]: 사실 4 - textbox "사실 4" [ref=f10e129]: authorized client의 열쇠에 session ID가 없기 때문에 같은 사용자가 두 브라우저에서 로그인하면 같은 항목을 보게 된다. - generic [ref=f10e130]: - button "위로" [ref=f10e131] - button "아래로" [ref=f10e132] - button "삭제" [ref=f10e133] - generic [ref=f10e134]: - generic [ref=f10e135]: - generic [ref=f10e136]: 사실 5 - textbox "사실 5" [ref=f10e137]: OAuth2-Proxy 구조는 server-side session store를 두지 않고 최소 정보만 담은 client-side cookie를 쓰게 되며, cookie 만료는 proxy 설정의 1 hour다. - generic [ref=f10e138]: - button "위로" [ref=f10e139] - button "아래로" [ref=f10e140] - button "삭제" [ref=f10e141] - generic [ref=f10e142]: - generic [ref=f10e143]: - generic [ref=f10e144]: 사실 6 - textbox "사실 6" [ref=f10e145]: 커밋된 테스트에 재시작이나 replica 이동 뒤 복구 계약이 없어서 지금 무엇을 바꿔도 회귀를 잡아 줄 검사가 없다. - generic [ref=f10e146]: - button "위로" [ref=f10e147] - button "아래로" [disabled] [ref=f10e148] - button "삭제" [ref=f10e149] - button "사실 추가" [ref=f10e150] - group "가정" [ref=f10e151]: - generic [ref=f10e153]: - generic [ref=f10e154]: - generic [ref=f10e155]: 가정 1 - textbox "가정 1" [ref=f10e156]: 운영에서는 인스턴스가 둘 이상이다. - generic [ref=f10e157]: - button "위로" [disabled] [ref=f10e158] - button "아래로" [ref=f10e159] - button "삭제" [ref=f10e160] - generic [ref=f10e161]: - generic [ref=f10e162]: - generic [ref=f10e163]: 가정 2 - textbox "가정 2" [ref=f10e164]: 재시작과 배포가 로그인 상태를 끊어서는 안 되는데, 지금 구조에서는 끊기게 된다. - generic [ref=f10e165]: - button "위로" [ref=f10e166] - button "아래로" [ref=f10e167] - button "삭제" [ref=f10e168] - generic [ref=f10e169]: - generic [ref=f10e170]: - generic [ref=f10e171]: 가정 3 - textbox "가정 3" [ref=f10e172]: 같은 사용자의 여러 브라우저 session이 서로의 token 항목을 덮어써서는 안 된다. - generic [ref=f10e173]: - button "위로" [ref=f10e174] - button "아래로" [disabled] [ref=f10e175] - button "삭제" [ref=f10e176] - button "가정 추가" [ref=f10e177] - group "미지수" [ref=f10e178]: - generic [ref=f10e180]: - generic [ref=f10e181]: - generic [ref=f10e182]: 미지수 1 - textbox "미지수 1" [ref=f10e183]: 재시작 뒤 로그인이 유지되는가. 지금은 안 된다는 것까지 알지만 무엇을 바꿔야 되는지는 정하지 않았다. - generic [ref=f10e184]: - button "위로" [disabled] [ref=f10e185] - button "아래로" [ref=f10e186] - button "삭제" [ref=f10e187] - generic [ref=f10e188]: - generic [ref=f10e189]: - generic [ref=f10e190]: 미지수 2 - textbox "미지수 2" [ref=f10e191]: 인스턴스가 바뀌어도 같은 session을 찾게 되는가. - generic [ref=f10e192]: - button "위로" [ref=f10e193] - button "아래로" [ref=f10e194] - button "삭제" [ref=f10e195] - generic [ref=f10e196]: - generic [ref=f10e197]: - generic [ref=f10e198]: 미지수 3 - textbox "미지수 3" [ref=f10e199]: 같은 사용자의 여러 session이 authorized client 항목을 공유하거나 덮어쓰게 되는가. 한쪽에서 로그아웃하면 다른 쪽도 끊기게 되는가. - generic [ref=f10e200]: - button "위로" [ref=f10e201] - button "아래로" [ref=f10e202] - button "삭제" [ref=f10e203] - generic [ref=f10e204]: - generic [ref=f10e205]: - generic [ref=f10e206]: 미지수 4 - textbox "미지수 4" [ref=f10e207]: 저장된 refresh token이 암호화되는가. 저장소를 여는 사람이 그 값을 그대로 읽게 되는가. - generic [ref=f10e208]: - button "위로" [ref=f10e209] - button "아래로" [ref=f10e210] - button "삭제" [ref=f10e211] - generic [ref=f10e212]: - generic [ref=f10e213]: - generic [ref=f10e214]: 미지수 5 - textbox "미지수 5" [ref=f10e215]: logout에서 HttpSession과 authorized client를 모두 정리하는가. 한쪽만 삭제했을 때 다음 요청이나 재로그인에서 어떤 상태가 복원되는가. - generic [ref=f10e216]: - button "위로" [ref=f10e217] - button "아래로" [ref=f10e218] - button "삭제" [ref=f10e219] - generic [ref=f10e220]: - generic [ref=f10e221]: - generic [ref=f10e222]: 미지수 6 - textbox "미지수 6" [ref=f10e223]: session 만료와 token 만료가 어긋나면 무엇이 먼저 실패하고 사용자 화면에는 어떻게 보이게 되는가. - generic [ref=f10e224]: - button "위로" [ref=f10e225] - button "아래로" [ref=f10e226] - button "삭제" [ref=f10e227] - generic [ref=f10e228]: - generic [ref=f10e229]: - generic [ref=f10e230]: 미지수 7 - textbox "미지수 7" [ref=f10e231]: OAuth2-Proxy 구조의 replica들이 같은 cookie secret을 어떻게 공유하고 교체하게 되는가. 교체하는 동안 로그인해 있던 사람은 어떻게 되는가. - generic [ref=f10e232]: - button "위로" [ref=f10e233] - button "아래로" [disabled] [ref=f10e234] - button "삭제" [ref=f10e235] - button "미지수 추가" [ref=f10e236] - group "제약" [ref=f10e237]: - generic [ref=f10e239]: - generic [ref=f10e240]: - generic [ref=f10e241]: 제약 1 - textbox "제약 1" [ref=f10e242]: 현재 예제는 단일 인스턴스로 실행하고 있어 replica 간 session 조회와 failover 동작은 아직 재현하지 않았다. - generic [ref=f10e243]: - button "위로" [disabled] [ref=f10e244] - button "아래로" [ref=f10e245] - button "삭제" [ref=f10e246] - generic [ref=f10e247]: - generic [ref=f10e248]: - generic [ref=f10e249]: 제약 2 - textbox "제약 2" [ref=f10e250]: authorized client의 key에는 session ID가 없다. session store를 shared store로 바꾸는 작업과 authorized client 저장 방식을 정하는 작업은 별도로 필요하다. - generic [ref=f10e251]: - button "위로" [ref=f10e252] - button "아래로" [ref=f10e253] - button "삭제" [ref=f10e254] - generic [ref=f10e255]: - generic [ref=f10e256]: - generic [ref=f10e257]: 제약 3 - textbox "제약 3" [ref=f10e258]: Resource Server의 8081이 host에도 열려 있어서 모든 client가 BFF만 거치도록 network에서 강제된 상태가 아니다. - generic [ref=f10e259]: - button "위로" [ref=f10e260] - button "아래로" [disabled] [ref=f10e261] - button "삭제" [ref=f10e262] - button "제약 추가" [ref=f10e263] - group "선택지" [ref=f10e264]: - generic [ref=f10e266]: - generic [ref=f10e267]: - generic [ref=f10e268]: 선택지 1 제목 - textbox "선택지 1 제목" [ref=f10e269]: 공유 저장소를 사용한다 - generic [ref=f10e270]: - generic [ref=f10e271]: 선택지 1 설명 - textbox "선택지 1 설명" [ref=f10e272]: HttpSession과 authorized client를 모두 외부 store에 두게 되면 인스턴스가 늘어도 같은 상태를 찾고 재시작도 견디게 된다. 공유 저장소를 사용하면 replica가 같은 상태를 조회할 수 있다. 반면 인증 경로가 저장소 가용성에 의존하므로 장애 처리, 직렬화 형식, token 암호화, session과 token의 만료 정합을 함께 설계해야 한다. - generic [ref=f10e273]: - button "위로" [disabled] [ref=f10e274] - button "아래로" [ref=f10e275] - button "삭제" [ref=f10e276] - generic [ref=f10e277]: - generic [ref=f10e278]: - generic [ref=f10e279]: 선택지 2 제목 - textbox "선택지 2 제목" [ref=f10e280]: session affinity로 묶는다 - generic [ref=f10e281]: - generic [ref=f10e282]: 선택지 2 설명 - textbox "선택지 2 설명" [ref=f10e283]: 같은 사용자를 같은 인스턴스로 보내게 되어서 코드를 거의 안 고쳐도 되고 저장소도 늘지 않는다. sticky session은 평상시 요청을 같은 인스턴스로 보낼 수 있지만 해당 인스턴스가 종료되면 process-local 상태도 함께 사용할 수 없게 된다. 배포나 오토스케일링처럼 인스턴스 교체가 잦은 환경에서는 별도 복구 전략이 필요하다. - generic [ref=f10e284]: - button "위로" [ref=f10e285] - button "아래로" [ref=f10e286] - button "삭제" [ref=f10e287] - generic [ref=f10e288]: - generic [ref=f10e289]: - generic [ref=f10e290]: 선택지 3 제목 - textbox "선택지 3 제목" [ref=f10e291]: 브라우저가 token을 들고 API를 직접 부르게 되돌린다 - generic [ref=f10e292]: - generic [ref=f10e293]: 선택지 3 설명 - textbox "선택지 3 설명" [ref=f10e294]: server에 상태를 두지 않게 되어서 공유 저장소도 affinity도 필요 없어지고 Resource Server는 요청마다 서명만 검증한다. SPA처럼 browser token을 사용하는 구조로 바꾸는 방법도 있지만, 브라우저에 OAuth token을 전달하지 않는 정책이 있다면 후보에서 제외한다. - generic [ref=f10e295]: - button "위로" [ref=f10e296] - button "아래로" [ref=f10e297] - button "삭제" [ref=f10e298] - generic [ref=f10e299]: - generic [ref=f10e300]: - generic [ref=f10e301]: 선택지 4 제목 - textbox "선택지 4 제목" [ref=f10e302]: 저장소 선택이 아니라 구조 변경 — 최소 정보만 담은 client-side cookie - generic [ref=f10e303]: - generic [ref=f10e304]: 선택지 4 설명 - textbox "선택지 4 설명" [ref=f10e305]: 이것은 저장소를 바꾸는 선택이 아니다. server-side store를 없애고 인증 상태를 cookie 자체에 담는 구조 변경이라서 앞의 세 후보와 같은 층에 놓고 비교할 수 없다. Forward-Auth로 전환하면 애플리케이션이 server-side OAuth token store를 운영하지 않아도 된다. 이 구조에서는 replica가 공유할 cookie secret과 edge identity header를 신뢰하기 위한 network·header 검증을 운영해야 한다. - generic [ref=f10e306]: - button "위로" [ref=f10e307] - button "아래로" [disabled] [ref=f10e308] - button "삭제" [ref=f10e309] - button "선택지 추가" [ref=f10e310] - generic [ref=f10e311]: - generic [ref=f10e312]: 다음 검증 - textbox "다음 검증" [ref=f10e313]: 인스턴스를 둘로 띄우고 순서대로 확인한다. 1. 한쪽에서 로그인한 뒤 다른 인스턴스로 요청을 보내 200이 유지되는지 본다. 2. 한 인스턴스를 재시작하고 같은 session cookie로 로그인 상태가 남는지 본다. 3. 같은 사용자로 두 브라우저에서 로그인해 authorized client 항목이 서로를 덮어쓰는지 본다. 4. 한쪽에서 logout한 뒤 다른 쪽 요청이 어떻게 되는지 본다. 5. session 만료를 token 만료보다 짧게, 다시 길게 두고 각 경우의 응답과 화면을 기록한다. 여기서 무엇이 깨지는지가 갈리게 되면 저장소 후보 비교로 넘어간다. - region [ref=f10e314]: - generic [ref=f10e315]: - paragraph [ref=f10e316]: LIVE - heading "즉시 미리보기" [level=2] [ref=f10e317] - generic [ref=f10e320]: - generic [ref=f10e321]: - navigation "문서 경로" [ref=f10e322]: - link "Open Question" [ref=f10e323] [cursor=pointer]: - /url: /explore/questions - generic [ref=f10e324]: / - generic [ref=f10e325]: OAuth/OIDC 인증 경계 - generic [ref=f10e326]: / - link "KeyCloak Patterns" [ref=f10e327] [cursor=pointer]: - /url: /projects/keycloak-patterns - heading "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [level=1] [ref=f10e328] - paragraph [ref=f10e329]: Mediator와 BFF는 로그인 상태와 token 상태를 열쇠가 다른 두 저장소에 나눠 두게 되는데 지금은 두 상태가 다 process 안에 있다. 인스턴스가 둘 이상인 운영에서 재시작과 이동, logout이 어떻게 동작해야 하는지 아직 정하지 않았다. - generic [ref=f10e330]: - generic [ref=f10e331]: - term [ref=f10e332]: 유형 - definition [ref=f10e333]: Open Question - generic [ref=f10e334]: - term [ref=f10e335]: 프로젝트 - definition [ref=f10e336]: KeyCloak Patterns - generic [ref=f10e337]: - term [ref=f10e338]: 게시 - definition [ref=f10e339]: 게시 전 - paragraph [ref=f10e340]: OPEN - article [ref=f10e341]: - region [ref=f10e342]: - heading "확인한 사실" [level=2] [ref=f10e343] - list [ref=f10e344]: - listitem [ref=f10e345]: Mediator와 BFF는 로그인 상태를 HttpSession에 두고 token은 OAuth2AuthorizedClientService에 두게 되는데, 두 저장소는 열쇠가 다르다. session은 session ID로 찾고 authorized client는 registration 이름과 principal name으로 찾는다. - listitem [ref=f10e346]: 현재 두 저장소는 Spring Boot 자동구성이 선택한 in-memory 구현을 사용한다. 코드에서 store bean을 직접 선언하지 않았기 때문에 실제 구현은 자동구성 결과를 함께 확인해야 한다. - listitem [ref=f10e347]: Spring Session과 Redis, JDBC token store 의존성이 없어서 두 상태가 모두 process 안에 있다. 그 instance가 종료되면 그 instance가 들고 있던 session과 authorized client는 사라진다. - listitem [ref=f10e348]: authorized client의 열쇠에 session ID가 없기 때문에 같은 사용자가 두 브라우저에서 로그인하면 같은 항목을 보게 된다. - listitem [ref=f10e349]: OAuth2-Proxy 구조는 server-side session store를 두지 않고 최소 정보만 담은 client-side cookie를 쓰게 되며, cookie 만료는 proxy 설정의 1 hour다. - listitem [ref=f10e350]: 커밋된 테스트에 재시작이나 replica 이동 뒤 복구 계약이 없어서 지금 무엇을 바꿔도 회귀를 잡아 줄 검사가 없다. - region [ref=f10e351]: - heading "가정" [level=2] [ref=f10e352] - list [ref=f10e353]: - listitem [ref=f10e354]: 운영에서는 인스턴스가 둘 이상이다. - listitem [ref=f10e355]: 재시작과 배포가 로그인 상태를 끊어서는 안 되는데, 지금 구조에서는 끊기게 된다. - listitem [ref=f10e356]: 같은 사용자의 여러 브라우저 session이 서로의 token 항목을 덮어써서는 안 된다. - region [ref=f10e357]: - heading "남은 미지수" [level=2] [ref=f10e358] - list [ref=f10e359]: - listitem [ref=f10e360]: 재시작 뒤 로그인이 유지되는가. 지금은 안 된다는 것까지 알지만 무엇을 바꿔야 되는지는 정하지 않았다. - listitem [ref=f10e361]: 인스턴스가 바뀌어도 같은 session을 찾게 되는가. - listitem [ref=f10e362]: 같은 사용자의 여러 session이 authorized client 항목을 공유하거나 덮어쓰게 되는가. 한쪽에서 로그아웃하면 다른 쪽도 끊기게 되는가. - listitem [ref=f10e363]: 저장된 refresh token이 암호화되는가. 저장소를 여는 사람이 그 값을 그대로 읽게 되는가. - listitem [ref=f10e364]: logout에서 HttpSession과 authorized client를 모두 정리하는가. 한쪽만 삭제했을 때 다음 요청이나 재로그인에서 어떤 상태가 복원되는가. - listitem [ref=f10e365]: session 만료와 token 만료가 어긋나면 무엇이 먼저 실패하고 사용자 화면에는 어떻게 보이게 되는가. - listitem [ref=f10e366]: OAuth2-Proxy 구조의 replica들이 같은 cookie secret을 어떻게 공유하고 교체하게 되는가. 교체하는 동안 로그인해 있던 사람은 어떻게 되는가. - region [ref=f10e367]: - heading "제약" [level=2] [ref=f10e368] - list [ref=f10e369]: - listitem [ref=f10e370]: 현재 예제는 단일 인스턴스로 실행하고 있어 replica 간 session 조회와 failover 동작은 아직 재현하지 않았다. - listitem [ref=f10e371]: authorized client의 key에는 session ID가 없다. session store를 shared store로 바꾸는 작업과 authorized client 저장 방식을 정하는 작업은 별도로 필요하다. - listitem [ref=f10e372]: Resource Server의 8081이 host에도 열려 있어서 모든 client가 BFF만 거치도록 network에서 강제된 상태가 아니다. - region [ref=f10e373]: - heading "검토한 선택지" [level=2] [ref=f10e374] - list [ref=f10e375]: - listitem [ref=f10e376]: - heading "공유 저장소를 사용한다" [level=3] [ref=f10e377] - paragraph [ref=f10e378]: HttpSession과 authorized client를 모두 외부 store에 두게 되면 인스턴스가 늘어도 같은 상태를 찾고 재시작도 견디게 된다. 공유 저장소를 사용하면 replica가 같은 상태를 조회할 수 있다. 반면 인증 경로가 저장소 가용성에 의존하므로 장애 처리, 직렬화 형식, token 암호화, session과 token의 만료 정합을 함께 설계해야 한다. - listitem [ref=f10e379]: - heading "session affinity로 묶는다" [level=3] [ref=f10e380] - paragraph [ref=f10e381]: 같은 사용자를 같은 인스턴스로 보내게 되어서 코드를 거의 안 고쳐도 되고 저장소도 늘지 않는다. sticky session은 평상시 요청을 같은 인스턴스로 보낼 수 있지만 해당 인스턴스가 종료되면 process-local 상태도 함께 사용할 수 없게 된다. 배포나 오토스케일링처럼 인스턴스 교체가 잦은 환경에서는 별도 복구 전략이 필요하다. - listitem [ref=f10e382]: - heading "브라우저가 token을 들고 API를 직접 부르게 되돌린다" [level=3] [ref=f10e383] - paragraph [ref=f10e384]: server에 상태를 두지 않게 되어서 공유 저장소도 affinity도 필요 없어지고 Resource Server는 요청마다 서명만 검증한다. SPA처럼 browser token을 사용하는 구조로 바꾸는 방법도 있지만, 브라우저에 OAuth token을 전달하지 않는 정책이 있다면 후보에서 제외한다. - listitem [ref=f10e385]: - heading "저장소 선택이 아니라 구조 변경 — 최소 정보만 담은 client-side cookie" [level=3] [ref=f10e386] - paragraph [ref=f10e387]: 이것은 저장소를 바꾸는 선택이 아니다. server-side store를 없애고 인증 상태를 cookie 자체에 담는 구조 변경이라서 앞의 세 후보와 같은 층에 놓고 비교할 수 없다. Forward-Auth로 전환하면 애플리케이션이 server-side OAuth token store를 운영하지 않아도 된다. 이 구조에서는 replica가 공유할 cookie secret과 edge identity header를 신뢰하기 위한 network·header 검증을 운영해야 한다. - region [ref=f10e388]: - paragraph [ref=f10e389]: Next - heading "다음 검증" [level=2] [ref=f10e390] - paragraph [ref=f10e391]: 인스턴스를 둘로 띄우고 순서대로 확인한다.1. 한쪽에서 로그인한 뒤 다른 인스턴스로 요청을 보내 200이 유지되는지 본다.2. 한 인스턴스를 재시작하고 같은 session cookie로 로그인 상태가 남는지 본다.3. 같은 사용자로 두 브라우저에서 로그인해 authorized client 항목이 서로를 덮어쓰는지 본다.4. 한쪽에서 logout한 뒤 다른 쪽 요청이 어떻게 되는지 본다.5. session 만료를 token 만료보다 짧게, 다시 길게 두고 각 경우의 응답과 화면을 기록한다.여기서 무엇이 깨지는지가 갈리게 되면 저장소 후보 비교로 넘어간다. - region [ref=f10e392]: - paragraph [ref=f10e393]: Relations - heading "이 기록과 연결된 맥락" [level=2] [ref=f10e394] - list [ref=f10e395]: - listitem [ref=f10e396]: - link "두 상태가 모두 process-local memory에 있다는 사실의 출처다. Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [ref=f10e397] [cursor=pointer]: - /url: /cases/bff-session-csrf-responsibility - generic [ref=f10e398]: 두 상태가 모두 process-local memory에 있다는 사실의 출처다. - strong [ref=f10e399]: Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정 - generic [ref=f10e400]: ↗ - listitem [ref=f10e401]: - link "같은 저장소 구성을 쓰는 다른 패턴이다. Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f10e402] [cursor=pointer]: - /url: /cases/split-custody-access-token - generic [ref=f10e403]: 같은 저장소 구성을 쓰는 다른 패턴이다. - strong [ref=f10e404]: Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출 - generic [ref=f10e405]: ↗ - complementary [ref=f10e406]: - heading "작업 상태" [level=2] [ref=f10e407] - status "편집 상태" [ref=f10e408]: 저장됨 - generic [ref=f10e409]: - generic [ref=f10e410]: - term [ref=f10e411]: 저장 버전 - definition [ref=f10e412]: "11" - generic [ref=f10e413]: - term [ref=f10e414]: 종류 - definition [ref=f10e415]: QUESTION - paragraph [ref=f10e416]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다. - generic [ref=f10e417]: - button "저장" [disabled] [ref=f10e418] - button "게시" [ref=f10e419] - paragraph [ref=f10e420]: 버전 11으로 저장했습니다.