Files
document-haness/.playwright-mcp/page-2026-08-26T11-32-15-064Z.yml

485 lines
36 KiB
YAML

- generic [ref=f8e3]:
- link "본문으로 건너뛰기" [ref=f8e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f8e5]:
- generic [ref=f8e6]:
- link "TechLog Studio" [ref=f8e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f8e8]: Studio
- navigation "Studio 주 탐색" [ref=f8e10]:
- link "작업본" [ref=f8e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f8e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f8e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f8e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f8e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f8e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f8e17]
- main [ref=f8e18]:
- generic [ref=f8e19]:
- generic [ref=f8e20]:
- region [ref=f8e21]:
- generic [ref=f8e22]:
- paragraph [ref=f8e23]: QUESTION · VERSION 9
- heading "문서 편집" [level=1] [ref=f8e24]
- paragraph [ref=f8e25]: BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
- region [ref=f8e26]:
- generic [ref=f8e27]:
- paragraph [ref=f8e28]: DOCUMENT
- heading "기본 정보" [level=2] [ref=f8e29]
- generic [ref=f8e30]:
- generic [ref=f8e31]:
- generic [ref=f8e32]: 제목
- textbox "제목" [ref=f8e33]: BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
- generic [ref=f8e34]:
- generic [ref=f8e35]: slug
- textbox "slug" [ref=f8e36]:
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
- text: bff-session-authorized-client-store
- generic [ref=f8e37]:
- generic [ref=f8e38]: 요약
- textbox "요약" [ref=f8e39]: session과 authorized client는 찾는 열쇠가 달라서 같은 저장소에 두는 것이 당연하지 않다. 저장소 후보는 Redis 쪽으로 기울어 있지만 token 암호화와 만료 정합, logout 정리를 확인하지 않았다.
- generic [ref=f8e40]:
- generic [ref=f8e41]: Topic
- combobox "Topic" [ref=f8e42]:
- option "선택하지 않음"
- option "OAuth/OIDC 인증 경계" [selected]
- generic [ref=f8e43]:
- generic [ref=f8e44]: Project
- combobox "Project" [ref=f8e45]:
- option "미지정"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- option "Liner N + 1문제"
- group "관계" [ref=f8e46]:
- generic [ref=f8e48]:
- generic [ref=f8e49]:
- generic [ref=f8e50]: 관계 1 대상
- combobox "관계 1 대상" [ref=f8e51]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [selected]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- 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에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" [disabled]
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f8e52]:
- generic [ref=f8e53]: 관계 1 이유
- textbox "관계 1 이유" [ref=f8e54]: 이 질문에서 저장소 부분만 떼어 낸 것이다.
- generic [ref=f8e55]:
- button "위로" [disabled] [ref=f8e56]
- button "아래로" [ref=f8e57]
- button "삭제" [ref=f8e58]
- generic [ref=f8e59]:
- generic [ref=f8e60]:
- generic [ref=f8e61]: 관계 2 대상
- combobox "관계 2 대상" [ref=f8e62]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- 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에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" [disabled]
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f8e63]:
- generic [ref=f8e64]: 관계 2 이유
- textbox "관계 2 이유" [ref=f8e65]: session과 authorized client의 열쇠가 다르다는 사실의 출처다.
- generic [ref=f8e66]:
- button "위로" [ref=f8e67]
- button "아래로" [ref=f8e68]
- button "삭제" [ref=f8e69]
- generic [ref=f8e70]:
- generic [ref=f8e71]:
- generic [ref=f8e72]: 관계 3 대상
- combobox "관계 3 대상" [ref=f8e73]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [selected]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- 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에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" [disabled]
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f8e74]:
- generic [ref=f8e75]: 관계 3 이유
- textbox "관계 3 이유" [ref=f8e76]: 이 기준의 저장소 항목이 이 질문의 답을 기다린다.
- generic [ref=f8e77]:
- button "위로" [ref=f8e78]
- button "아래로" [ref=f8e79]
- button "삭제" [ref=f8e80]
- generic [ref=f8e81]:
- generic [ref=f8e82]:
- generic [ref=f8e83]: 관계 4 대상
- combobox "관계 4 대상" [ref=f8e84]:
- option "대상 선택"
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
- option "BFF 인증 구조 설계 기준" [disabled]
- option "BFF가 OAuth Token을 관리하는 조건"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
- 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에 노출"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" [selected]
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계"
- generic [ref=f8e85]:
- generic [ref=f8e86]: 관계 4 이유
- textbox "관계 4 이유" [ref=f8e87]: 저장소를 공유한 뒤에야 replica 경쟁이 재현된다.
- generic [ref=f8e88]:
- button "위로" [ref=f8e89]
- button "아래로" [disabled] [ref=f8e90]
- button "삭제" [ref=f8e91]
- button "관계 추가" [ref=f8e92]
- region [ref=f8e93]:
- generic [ref=f8e94]:
- paragraph [ref=f8e95]: QUESTION
- heading "판단과 다음 검증" [level=2] [ref=f8e96]
- generic [ref=f8e97]:
- generic [ref=f8e98]: 질문 상태
- combobox "질문 상태" [ref=f8e99]:
- option "아직 정하지 않음"
- option "OPEN" [selected]
- option "RESOLVED"
- group "사실" [ref=f8e100]:
- generic [ref=f8e102]:
- generic [ref=f8e103]:
- generic [ref=f8e104]: 사실 1
- textbox "사실 1" [ref=f8e105]: 현재 구성에 Spring Session과 Redis, JDBC repository, 암호화 token store가 없다.
- generic [ref=f8e106]:
- button "위로" [disabled] [ref=f8e107]
- button "아래로" [ref=f8e108]
- button "삭제" [ref=f8e109]
- generic [ref=f8e110]:
- generic [ref=f8e111]:
- generic [ref=f8e112]: 사실 2
- textbox "사실 2" [ref=f8e113]: 현재 HttpSession은 servlet container의 in-memory 구현을 사용하므로 해당 process가 종료되면 session 데이터도 유지되지 않는다.
- generic [ref=f8e114]:
- button "위로" [ref=f8e115]
- button "아래로" [ref=f8e116]
- button "삭제" [ref=f8e117]
- generic [ref=f8e118]:
- generic [ref=f8e119]:
- generic [ref=f8e120]: 사실 3
- textbox "사실 3" [ref=f8e121]: OAuth2AuthorizedClientService도 자동구성이 고르는 in-memory 구현이고 코드가 직접 선언하지 않는다.
- generic [ref=f8e122]:
- button "위로" [ref=f8e123]
- button "아래로" [ref=f8e124]
- button "삭제" [ref=f8e125]
- generic [ref=f8e126]:
- generic [ref=f8e127]:
- generic [ref=f8e128]: 사실 4
- textbox "사실 4" [ref=f8e129]: session은 session ID로 조회하고 authorized client는 registration 이름과 principal name으로 조회한다. 두 저장 구조를 shared store로 전환할 때 각각 따로 설계해야 한다.
- generic [ref=f8e130]:
- button "위로" [ref=f8e131]
- button "아래로" [ref=f8e132]
- button "삭제" [ref=f8e133]
- generic [ref=f8e134]:
- generic [ref=f8e135]:
- generic [ref=f8e136]: 사실 5
- textbox "사실 5" [ref=f8e137]: authorized client manager에 authorization-code와 refresh-token provider가 함께 구성돼 있어서, 저장소를 공유하게 되면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있게 된다.
- generic [ref=f8e138]:
- button "위로" [ref=f8e139]
- button "아래로" [disabled] [ref=f8e140]
- button "삭제" [ref=f8e141]
- button "사실 추가" [ref=f8e142]
- group "가정" [ref=f8e143]:
- generic [ref=f8e145]:
- generic [ref=f8e146]:
- generic [ref=f8e147]: 가정 1
- textbox "가정 1" [ref=f8e148]: 두 상태를 같은 저장소에 둘 필요는 없다.
- generic [ref=f8e149]:
- button "위로" [disabled] [ref=f8e150]
- button "아래로" [ref=f8e151]
- button "삭제" [ref=f8e152]
- generic [ref=f8e153]:
- generic [ref=f8e154]:
- generic [ref=f8e155]: 가정 2
- textbox "가정 2" [ref=f8e156]: 저장된 refresh token을 평문으로 두면 안 된다.
- generic [ref=f8e157]:
- button "위로" [ref=f8e158]
- button "아래로" [ref=f8e159]
- button "삭제" [ref=f8e160]
- generic [ref=f8e161]:
- generic [ref=f8e162]:
- generic [ref=f8e163]: 가정 3
- textbox "가정 3" [ref=f8e164]: session 만료와 token 만료 중 하나가 먼저 오게 되면 그 순간의 동작이 정의돼 있어야 한다.
- generic [ref=f8e165]:
- button "위로" [ref=f8e166]
- button "아래로" [disabled] [ref=f8e167]
- button "삭제" [ref=f8e168]
- button "가정 추가" [ref=f8e169]
- group "미지수" [ref=f8e170]:
- generic [ref=f8e172]:
- generic [ref=f8e173]:
- generic [ref=f8e174]: 미지수 1
- textbox "미지수 1" [ref=f8e175]: Redis와 JDBC 중 무엇이 이 상태의 접근 패턴에 맞는가. 요청마다 읽는 값과 가끔 읽는 값이 섞여 있다.
- generic [ref=f8e176]:
- button "위로" [disabled] [ref=f8e177]
- button "아래로" [ref=f8e178]
- button "삭제" [ref=f8e179]
- generic [ref=f8e180]:
- generic [ref=f8e181]:
- generic [ref=f8e182]: 미지수 2
- textbox "미지수 2" [ref=f8e183]: session과 authorized client를 같은 store에 둘지 나눌지.
- generic [ref=f8e184]:
- button "위로" [ref=f8e185]
- button "아래로" [ref=f8e186]
- button "삭제" [ref=f8e187]
- generic [ref=f8e188]:
- generic [ref=f8e189]:
- generic [ref=f8e190]: 미지수 3
- textbox "미지수 3" [ref=f8e191]: 암호화 key를 어디에 두고 어떻게 교체하게 되는가. 교체하는 동안 이전 key로 저장된 값은 어떻게 읽는가.
- generic [ref=f8e192]:
- button "위로" [ref=f8e193]
- button "아래로" [ref=f8e194]
- button "삭제" [ref=f8e195]
- generic [ref=f8e196]:
- generic [ref=f8e197]:
- generic [ref=f8e198]: 미지수 4
- textbox "미지수 4" [ref=f8e199]: session TTL과 refresh token 수명 중 어느 것을 기준으로 만료를 맞추게 되는가.
- generic [ref=f8e200]:
- button "위로" [ref=f8e201]
- button "아래로" [ref=f8e202]
- button "삭제" [ref=f8e203]
- generic [ref=f8e204]:
- generic [ref=f8e205]:
- generic [ref=f8e206]: 미지수 5
- textbox "미지수 5" [ref=f8e207]: 열쇠가 다른 두 store를 logout에서 어떻게 한 번에 지우게 되는가.
- generic [ref=f8e208]:
- button "위로" [ref=f8e209]
- button "아래로" [ref=f8e210]
- button "삭제" [ref=f8e211]
- generic [ref=f8e212]:
- generic [ref=f8e213]:
- generic [ref=f8e214]: 미지수 6
- textbox "미지수 6" [ref=f8e215]: sticky session이 durable store의 대안이 되는가 보완이 되는가.
- generic [ref=f8e216]:
- button "위로" [ref=f8e217]
- button "아래로" [disabled] [ref=f8e218]
- button "삭제" [ref=f8e219]
- button "미지수 추가" [ref=f8e220]
- group "제약" [ref=f8e221]:
- generic [ref=f8e223]:
- generic [ref=f8e224]:
- generic [ref=f8e225]: 제약 1
- textbox "제약 1" [ref=f8e226]: authorized client의 열쇠에 session ID가 없어서 session만 공유해도 같은 사용자의 여러 session이 같은 token 항목을 보게 된다.
- generic [ref=f8e227]:
- button "위로" [disabled] [ref=f8e228]
- button "아래로" [ref=f8e229]
- button "삭제" [ref=f8e230]
- generic [ref=f8e231]:
- generic [ref=f8e232]:
- generic [ref=f8e233]: 제약 2
- textbox "제약 2" [ref=f8e234]: 커밋된 테스트에 저장소 관련 계약이 없어서 어느 후보를 골라도 지금은 회귀를 잡아 줄 검사가 없다.
- generic [ref=f8e235]:
- button "위로" [ref=f8e236]
- button "아래로" [ref=f8e237]
- button "삭제" [ref=f8e238]
- generic [ref=f8e239]:
- generic [ref=f8e240]:
- generic [ref=f8e241]: 제약 3
- textbox "제약 3" [ref=f8e242]: 모든 UI 요청이 BFF를 지나기 때문에 저장소 지연이 화면 지연으로 바로 드러나게 된다.
- generic [ref=f8e243]:
- button "위로" [ref=f8e244]
- button "아래로" [disabled] [ref=f8e245]
- button "삭제" [ref=f8e246]
- button "제약 추가" [ref=f8e247]
- group "선택지" [ref=f8e248]:
- generic [ref=f8e250]:
- generic [ref=f8e251]:
- generic [ref=f8e252]: 선택지 1 제목
- textbox "선택지 1 제목" [ref=f8e253]: 유력 후보 — session과 authorized client를 모두 Redis에 둔다
- generic [ref=f8e254]:
- generic [ref=f8e255]: 선택지 1 설명
- textbox "선택지 1 설명" [ref=f8e256]: Spring Session Redis와 Redis authorized-client repository를 쓰게 되면 만료를 store가 관리해 주고 인스턴스를 늘리기도 쉬워진다. Redis를 사용하면 모든 replica가 같은 session과 authorized client를 조회할 수 있다. 인증 경로가 Redis 가용성에 의존하게 되며, access·refresh token 저장 시 암호화 여부와 key 관리 방식도 정해야 한다.
- generic [ref=f8e257]:
- button "위로" [disabled] [ref=f8e258]
- button "아래로" [ref=f8e259]
- button "삭제" [ref=f8e260]
- generic [ref=f8e261]:
- generic [ref=f8e262]:
- generic [ref=f8e263]: 선택지 2 제목
- textbox "선택지 2 제목" [ref=f8e264]: session과 authorized client를 모두 JDBC에 둔다
- generic [ref=f8e265]:
- generic [ref=f8e266]: 선택지 2 설명
- textbox "선택지 2 설명" [ref=f8e267]: 이미 운영 중인 DB를 쓴다. 백업과 감사 절차가 그 DB에 이미 있다면 그만큼 새로 만들 것이 줄어든다. JDBC를 사용하면 기존 관계형 DB 운영 체계를 활용할 수 있지만 인증 요청마다 DB 조회가 발생한다. 만료 데이터 정리와 session 조회 지연도 운영 항목으로 포함해야 한다.
- generic [ref=f8e268]:
- button "위로" [ref=f8e269]
- button "아래로" [ref=f8e270]
- button "삭제" [ref=f8e271]
- generic [ref=f8e272]:
- generic [ref=f8e273]:
- generic [ref=f8e274]: 선택지 3 제목
- textbox "선택지 3 제목" [ref=f8e275]: 변경이 가장 작은 안 — session만 공유하고 sticky session을 쓴다
- generic [ref=f8e276]:
- generic [ref=f8e277]: 선택지 3 설명
- textbox "선택지 3 설명" [ref=f8e278]: Spring Session만 붙이면 되어서 변경이 가장 적다. sticky session만 적용하면 authorized client는 여전히 process-local 상태다. 요청이 다른 인스턴스로 라우팅되거나 해당 인스턴스가 종료될 때 session과 token 상태의 정합을 보장하기 어렵다.
- generic [ref=f8e279]:
- button "위로" [ref=f8e280]
- button "아래로" [ref=f8e281]
- button "삭제" [ref=f8e282]
- generic [ref=f8e283]:
- generic [ref=f8e284]:
- generic [ref=f8e285]: 선택지 4 제목
- textbox "선택지 4 제목" [ref=f8e286]: session은 Redis, token은 암호화한 JDBC에 둔다
- generic [ref=f8e287]:
- generic [ref=f8e288]: 선택지 4 설명
- textbox "선택지 4 설명" [ref=f8e289]: 요청마다 읽는 session은 빠른 저장소에 두고 오래 보관하면서 암호화가 필요한 token은 DB에 두게 되어서 접근 패턴에 맞다. session과 authorized client를 서로 다른 저장소에 두면 각각의 TTL과 logout 정리 순서를 맞춰야 하고 운영 대상 저장소도 하나 늘어난다.
- generic [ref=f8e290]:
- button "위로" [ref=f8e291]
- button "아래로" [disabled] [ref=f8e292]
- button "삭제" [ref=f8e293]
- button "선택지 추가" [ref=f8e294]
- generic [ref=f8e295]:
- generic [ref=f8e296]: 다음 검증
- textbox "다음 검증" [ref=f8e297]: 후보마다 같은 입력으로 재서 비교한다. 1. 인스턴스 두 대에서 로그인 유지와 재시작 복구가 되는지 본다. 2. 저장소를 직접 열어 refresh token이 평문으로 남는지 확인한다. 3. session TTL과 token 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다. 4. logout 뒤 두 store에 잔여 항목이 없는지 확인한다. 5. 저장소를 끊은 상태에서 로그인과 API 호출이 어떤 오류를 내는지 본다. 암호화 key 교체 절차는 후보를 고른 뒤에 따로 설계한다.
- region [ref=f8e298]:
- generic [ref=f8e299]:
- paragraph [ref=f8e300]: LIVE
- heading "즉시 미리보기" [level=2] [ref=f8e301]
- generic [ref=f8e304]:
- generic [ref=f8e305]:
- navigation "문서 경로" [ref=f8e306]:
- link "Open Question" [ref=f8e307] [cursor=pointer]:
- /url: /explore/questions
- generic [ref=f8e308]: /
- generic [ref=f8e309]: OAuth/OIDC 인증 경계
- generic [ref=f8e310]: /
- link "KeyCloak Patterns" [ref=f8e311] [cursor=pointer]:
- /url: /projects/keycloak-patterns
- heading "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [level=1] [ref=f8e312]
- paragraph [ref=f8e313]: session과 authorized client는 찾는 열쇠가 달라서 같은 저장소에 두는 것이 당연하지 않다. 저장소 후보는 Redis 쪽으로 기울어 있지만 token 암호화와 만료 정합, logout 정리를 확인하지 않았다.
- generic [ref=f8e314]:
- generic [ref=f8e315]:
- term [ref=f8e316]: 유형
- definition [ref=f8e317]: Open Question
- generic [ref=f8e318]:
- term [ref=f8e319]: 프로젝트
- definition [ref=f8e320]: KeyCloak Patterns
- generic [ref=f8e321]:
- term [ref=f8e322]: 게시
- definition [ref=f8e323]: 게시 전
- paragraph [ref=f8e324]: OPEN
- article [ref=f8e325]:
- region [ref=f8e326]:
- heading "확인한 사실" [level=2] [ref=f8e327]
- list [ref=f8e328]:
- listitem [ref=f8e329]: 현재 구성에 Spring Session과 Redis, JDBC repository, 암호화 token store가 없다.
- listitem [ref=f8e330]: 현재 HttpSession은 servlet container의 in-memory 구현을 사용하므로 해당 process가 종료되면 session 데이터도 유지되지 않는다.
- listitem [ref=f8e331]: OAuth2AuthorizedClientService도 자동구성이 고르는 in-memory 구현이고 코드가 직접 선언하지 않는다.
- listitem [ref=f8e332]: session은 session ID로 조회하고 authorized client는 registration 이름과 principal name으로 조회한다. 두 저장 구조를 shared store로 전환할 때 각각 따로 설계해야 한다.
- listitem [ref=f8e333]: authorized client manager에 authorization-code와 refresh-token provider가 함께 구성돼 있어서, 저장소를 공유하게 되면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있게 된다.
- region [ref=f8e334]:
- heading "가정" [level=2] [ref=f8e335]
- list [ref=f8e336]:
- listitem [ref=f8e337]: 두 상태를 같은 저장소에 둘 필요는 없다.
- listitem [ref=f8e338]: 저장된 refresh token을 평문으로 두면 안 된다.
- listitem [ref=f8e339]: session 만료와 token 만료 중 하나가 먼저 오게 되면 그 순간의 동작이 정의돼 있어야 한다.
- region [ref=f8e340]:
- heading "남은 미지수" [level=2] [ref=f8e341]
- list [ref=f8e342]:
- listitem [ref=f8e343]: Redis와 JDBC 중 무엇이 이 상태의 접근 패턴에 맞는가. 요청마다 읽는 값과 가끔 읽는 값이 섞여 있다.
- listitem [ref=f8e344]: session과 authorized client를 같은 store에 둘지 나눌지.
- listitem [ref=f8e345]: 암호화 key를 어디에 두고 어떻게 교체하게 되는가. 교체하는 동안 이전 key로 저장된 값은 어떻게 읽는가.
- listitem [ref=f8e346]: session TTL과 refresh token 수명 중 어느 것을 기준으로 만료를 맞추게 되는가.
- listitem [ref=f8e347]: 열쇠가 다른 두 store를 logout에서 어떻게 한 번에 지우게 되는가.
- listitem [ref=f8e348]: sticky session이 durable store의 대안이 되는가 보완이 되는가.
- region [ref=f8e349]:
- heading "제약" [level=2] [ref=f8e350]
- list [ref=f8e351]:
- listitem [ref=f8e352]: authorized client의 열쇠에 session ID가 없어서 session만 공유해도 같은 사용자의 여러 session이 같은 token 항목을 보게 된다.
- listitem [ref=f8e353]: 커밋된 테스트에 저장소 관련 계약이 없어서 어느 후보를 골라도 지금은 회귀를 잡아 줄 검사가 없다.
- listitem [ref=f8e354]: 모든 UI 요청이 BFF를 지나기 때문에 저장소 지연이 화면 지연으로 바로 드러나게 된다.
- region [ref=f8e355]:
- heading "검토한 선택지" [level=2] [ref=f8e356]
- list [ref=f8e357]:
- listitem [ref=f8e358]:
- heading "유력 후보 — session과 authorized client를 모두 Redis에 둔다" [level=3] [ref=f8e359]
- paragraph [ref=f8e360]: Spring Session Redis와 Redis authorized-client repository를 쓰게 되면 만료를 store가 관리해 주고 인스턴스를 늘리기도 쉬워진다. Redis를 사용하면 모든 replica가 같은 session과 authorized client를 조회할 수 있다. 인증 경로가 Redis 가용성에 의존하게 되며, access·refresh token 저장 시 암호화 여부와 key 관리 방식도 정해야 한다.
- listitem [ref=f8e361]:
- heading "session과 authorized client를 모두 JDBC에 둔다" [level=3] [ref=f8e362]
- paragraph [ref=f8e363]: 이미 운영 중인 DB를 쓴다. 백업과 감사 절차가 그 DB에 이미 있다면 그만큼 새로 만들 것이 줄어든다. JDBC를 사용하면 기존 관계형 DB 운영 체계를 활용할 수 있지만 인증 요청마다 DB 조회가 발생한다. 만료 데이터 정리와 session 조회 지연도 운영 항목으로 포함해야 한다.
- listitem [ref=f8e364]:
- heading "변경이 가장 작은 안 — session만 공유하고 sticky session을 쓴다" [level=3] [ref=f8e365]
- paragraph [ref=f8e366]: Spring Session만 붙이면 되어서 변경이 가장 적다. sticky session만 적용하면 authorized client는 여전히 process-local 상태다. 요청이 다른 인스턴스로 라우팅되거나 해당 인스턴스가 종료될 때 session과 token 상태의 정합을 보장하기 어렵다.
- listitem [ref=f8e367]:
- heading "session은 Redis, token은 암호화한 JDBC에 둔다" [level=3] [ref=f8e368]
- paragraph [ref=f8e369]: 요청마다 읽는 session은 빠른 저장소에 두고 오래 보관하면서 암호화가 필요한 token은 DB에 두게 되어서 접근 패턴에 맞다. session과 authorized client를 서로 다른 저장소에 두면 각각의 TTL과 logout 정리 순서를 맞춰야 하고 운영 대상 저장소도 하나 늘어난다.
- region [ref=f8e370]:
- paragraph [ref=f8e371]: Next
- heading "다음 검증" [level=2] [ref=f8e372]
- paragraph [ref=f8e373]: 후보마다 같은 입력으로 재서 비교한다.1. 인스턴스 두 대에서 로그인 유지와 재시작 복구가 되는지 본다.2. 저장소를 직접 열어 refresh token이 평문으로 남는지 확인한다.3. session TTL과 token 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다.4. logout 뒤 두 store에 잔여 항목이 없는지 확인한다.5. 저장소를 끊은 상태에서 로그인과 API 호출이 어떤 오류를 내는지 본다.암호화 key 교체 절차는 후보를 고른 뒤에 따로 설계한다.
- region [ref=f8e374]:
- paragraph [ref=f8e375]: Relations
- heading "이 기록과 연결된 맥락" [level=2] [ref=f8e376]
- list [ref=f8e377]:
- listitem [ref=f8e378]:
- link "session과 authorized client의 열쇠가 다르다는 사실의 출처다. Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [ref=f8e379] [cursor=pointer]:
- /url: /cases/bff-session-csrf-responsibility
- generic [ref=f8e380]: session과 authorized client의 열쇠가 다르다는 사실의 출처다.
- strong [ref=f8e381]: Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정
- generic [ref=f8e382]:
- complementary [ref=f8e383]:
- heading "작업 상태" [level=2] [ref=f8e384]
- status "편집 상태" [ref=f8e385]: 저장됨
- generic [ref=f8e386]:
- generic [ref=f8e387]:
- term [ref=f8e388]: 저장 버전
- definition [ref=f8e389]: "9"
- generic [ref=f8e390]:
- term [ref=f8e391]: 종류
- definition [ref=f8e392]: QUESTION
- paragraph [ref=f8e393]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
- generic [ref=f8e394]:
- button "저장" [disabled] [ref=f8e395]
- button "게시" [ref=f8e396]
- paragraph [ref=f8e397]: 버전 9으로 저장했습니다.