485 lines
37 KiB
YAML
485 lines
37 KiB
YAML
- generic [ref=f9e3]:
|
|
- link "본문으로 건너뛰기" [ref=f9e4] [cursor=pointer]:
|
|
- /url: "#main-content"
|
|
- banner [ref=f9e5]:
|
|
- generic [ref=f9e6]:
|
|
- link "TechLog Studio" [ref=f9e7] [cursor=pointer]:
|
|
- /url: /studio
|
|
- text: TechLog
|
|
- generic [ref=f9e8]: Studio
|
|
- navigation "Studio 주 탐색" [ref=f9e10]:
|
|
- link "작업본" [ref=f9e11] [cursor=pointer]:
|
|
- /url: /studio/documents
|
|
- link "게시 기록" [ref=f9e12] [cursor=pointer]:
|
|
- /url: /studio/publications
|
|
- link "새 문서" [ref=f9e13] [cursor=pointer]:
|
|
- /url: /studio/documents/new
|
|
- link "주제·프로젝트" [ref=f9e14] [cursor=pointer]:
|
|
- /url: /studio/taxonomy
|
|
- link "릴리즈" [ref=f9e15] [cursor=pointer]:
|
|
- /url: /studio/releases
|
|
- link "공개 사이트 보기" [ref=f9e16] [cursor=pointer]:
|
|
- /url: /
|
|
- button "로그아웃" [ref=f9e17]
|
|
- main [ref=f9e18]:
|
|
- generic [ref=f9e19]:
|
|
- generic [ref=f9e20]:
|
|
- region [ref=f9e21]:
|
|
- generic [ref=f9e22]:
|
|
- paragraph [ref=f9e23]: QUESTION · VERSION 11
|
|
- heading "문서 편집" [level=1] [ref=f9e24]
|
|
- paragraph [ref=f9e25]: Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가
|
|
- region [ref=f9e26]:
|
|
- generic [ref=f9e27]:
|
|
- paragraph [ref=f9e28]: DOCUMENT
|
|
- heading "기본 정보" [level=2] [ref=f9e29]
|
|
- generic [ref=f9e30]:
|
|
- generic [ref=f9e31]:
|
|
- generic [ref=f9e32]: 제목
|
|
- textbox "제목" [ref=f9e33]: Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가
|
|
- generic [ref=f9e34]:
|
|
- generic [ref=f9e35]: slug
|
|
- textbox "slug" [ref=f9e36]:
|
|
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
|
|
- text: refresh-rotation-replica-contention
|
|
- generic [ref=f9e37]:
|
|
- generic [ref=f9e38]: 요약
|
|
- textbox "요약" [ref=f9e39]: realm이 refresh token rotation과 재사용 허용 0회를 쓴다. 두 replica가 같은 refresh token으로 동시에 갱신할 수 있고, 그때 두 번째 사용이 거부될 가능성이 있다. 실제 Keycloak 응답과 session 영향은 아직 재현하지 않았다.
|
|
- generic [ref=f9e40]:
|
|
- generic [ref=f9e41]: Topic
|
|
- combobox "Topic" [ref=f9e42]:
|
|
- option "선택하지 않음"
|
|
- option "OAuth/OIDC 인증 경계" [selected]
|
|
- generic [ref=f9e43]:
|
|
- generic [ref=f9e44]: Project
|
|
- combobox "Project" [ref=f9e45]:
|
|
- option "미지정"
|
|
- option "Backend Clean Architecture"
|
|
- option "KeyCloak Patterns" [selected]
|
|
- option "Liner N + 1문제"
|
|
- group "관계" [ref=f9e46]:
|
|
- generic [ref=f9e48]:
|
|
- generic [ref=f9e49]:
|
|
- generic [ref=f9e50]: 관계 1 대상
|
|
- combobox "관계 1 대상" [ref=f9e51]:
|
|
- option "대상 선택"
|
|
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
|
|
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
|
|
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
|
|
- option "BFF 인증 구조 설계 기준" [disabled]
|
|
- option "BFF가 OAuth Token을 관리하는 조건"
|
|
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [selected]
|
|
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
|
|
- 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=f9e52]:
|
|
- generic [ref=f9e53]: 관계 1 이유
|
|
- textbox "관계 1 이유" [ref=f9e54]: 저장소 결정이 이 질문보다 앞선다.
|
|
- generic [ref=f9e55]:
|
|
- button "위로" [disabled] [ref=f9e56]
|
|
- button "아래로" [ref=f9e57]
|
|
- button "삭제" [ref=f9e58]
|
|
- generic [ref=f9e59]:
|
|
- generic [ref=f9e60]:
|
|
- generic [ref=f9e61]: 관계 2 대상
|
|
- combobox "관계 2 대상" [ref=f9e62]:
|
|
- option "대상 선택"
|
|
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
|
|
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
|
|
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
|
|
- option "BFF 인증 구조 설계 기준" [disabled]
|
|
- option "BFF가 OAuth Token을 관리하는 조건"
|
|
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
|
|
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
|
|
- 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=f9e63]:
|
|
- generic [ref=f9e64]: 관계 2 이유
|
|
- textbox "관계 2 이유" [ref=f9e65]: rotation과 재사용 0회를 쓰는 구성의 출처다.
|
|
- generic [ref=f9e66]:
|
|
- button "위로" [ref=f9e67]
|
|
- button "아래로" [ref=f9e68]
|
|
- button "삭제" [ref=f9e69]
|
|
- generic [ref=f9e70]:
|
|
- generic [ref=f9e71]:
|
|
- generic [ref=f9e72]: 관계 3 대상
|
|
- combobox "관계 3 대상" [ref=f9e73]:
|
|
- option "대상 선택"
|
|
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
|
|
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [selected]
|
|
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
|
|
- option "BFF 인증 구조 설계 기준" [disabled]
|
|
- option "BFF가 OAuth Token을 관리하는 조건"
|
|
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
|
|
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
|
|
- 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=f9e74]:
|
|
- generic [ref=f9e75]: 관계 3 이유
|
|
- textbox "관계 3 이유" [ref=f9e76]: 다중 인스턴스 운영이 이 경쟁의 전제다.
|
|
- generic [ref=f9e77]:
|
|
- button "위로" [ref=f9e78]
|
|
- button "아래로" [ref=f9e79]
|
|
- button "삭제" [ref=f9e80]
|
|
- generic [ref=f9e81]:
|
|
- generic [ref=f9e82]:
|
|
- generic [ref=f9e83]: 관계 4 대상
|
|
- combobox "관계 4 대상" [ref=f9e84]:
|
|
- option "대상 선택"
|
|
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
|
|
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" [disabled]
|
|
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
|
|
- option "BFF 인증 구조 설계 기준" [selected]
|
|
- option "BFF가 OAuth Token을 관리하는 조건"
|
|
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [disabled]
|
|
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
|
|
- 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=f9e85]:
|
|
- generic [ref=f9e86]: 관계 4 이유
|
|
- textbox "관계 4 이유" [ref=f9e87]: 갱신 실패를 화면 오류로 바꾸는 규칙이 이 기준의 항목이다.
|
|
- generic [ref=f9e88]:
|
|
- button "위로" [ref=f9e89]
|
|
- button "아래로" [disabled] [ref=f9e90]
|
|
- button "삭제" [ref=f9e91]
|
|
- button "관계 추가" [ref=f9e92]
|
|
- region [ref=f9e93]:
|
|
- generic [ref=f9e94]:
|
|
- paragraph [ref=f9e95]: QUESTION
|
|
- heading "판단과 다음 검증" [level=2] [ref=f9e96]
|
|
- generic [ref=f9e97]:
|
|
- generic [ref=f9e98]: 질문 상태
|
|
- combobox "질문 상태" [ref=f9e99]:
|
|
- option "아직 정하지 않음"
|
|
- option "OPEN" [selected]
|
|
- option "RESOLVED"
|
|
- group "사실" [ref=f9e100]:
|
|
- generic [ref=f9e102]:
|
|
- generic [ref=f9e103]:
|
|
- generic [ref=f9e104]: 사실 1
|
|
- textbox "사실 1" [ref=f9e105]: realm은 refresh token rotation과 재사용 허용 0회를 쓰게 되어서, 한 번 갱신하면 이전 refresh token은 바로 무효가 된다.
|
|
- generic [ref=f9e106]:
|
|
- button "위로" [disabled] [ref=f9e107]
|
|
- button "아래로" [ref=f9e108]
|
|
- button "삭제" [ref=f9e109]
|
|
- generic [ref=f9e110]:
|
|
- generic [ref=f9e111]:
|
|
- generic [ref=f9e112]: 사실 2
|
|
- textbox "사실 2" [ref=f9e113]: 커밋된 테스트는 새 refresh token 발급과 이전 token 거부, revocation 뒤 refresh 실패를 확인하는데 모두 한 주체가 순서대로 부르는 경우다.
|
|
- generic [ref=f9e114]:
|
|
- button "위로" [ref=f9e115]
|
|
- button "아래로" [ref=f9e116]
|
|
- button "삭제" [ref=f9e117]
|
|
- generic [ref=f9e118]:
|
|
- generic [ref=f9e119]:
|
|
- generic [ref=f9e120]: 사실 3
|
|
- textbox "사실 3" [ref=f9e121]: authorized client manager에는 refresh-token provider가 구성되어 있어 access token 만료 시 refresh를 시도할 수 있다.
|
|
- generic [ref=f9e122]:
|
|
- button "위로" [ref=f9e123]
|
|
- button "아래로" [ref=f9e124]
|
|
- button "삭제" [ref=f9e125]
|
|
- generic [ref=f9e126]:
|
|
- generic [ref=f9e127]:
|
|
- generic [ref=f9e128]: 사실 4
|
|
- textbox "사실 4" [ref=f9e129]: 다만 만료를 기다려 실제 갱신이 성공하고 새 token이 저장되는지까지는 확인하지 않았다.
|
|
- generic [ref=f9e130]:
|
|
- button "위로" [ref=f9e131]
|
|
- button "아래로" [ref=f9e132]
|
|
- button "삭제" [ref=f9e133]
|
|
- generic [ref=f9e134]:
|
|
- generic [ref=f9e135]:
|
|
- generic [ref=f9e136]: 사실 5
|
|
- textbox "사실 5" [ref=f9e137]: 현재 authorized client 저장소는 process-local이라 replica가 같은 refresh token 상태를 공유하지 않는다. 따라서 이번 단일 인스턴스 검증에서는 동시 refresh 경쟁을 재현하지 않았다.
|
|
- generic [ref=f9e138]:
|
|
- button "위로" [ref=f9e139]
|
|
- button "아래로" [ref=f9e140]
|
|
- button "삭제" [ref=f9e141]
|
|
- generic [ref=f9e142]:
|
|
- generic [ref=f9e143]:
|
|
- generic [ref=f9e144]: 사실 6
|
|
- textbox "사실 6" [ref=f9e145]: 이미 발급된 access token은 만료 전까지 API에서 계속 통하기 때문에, 갱신이 실패해도 그동안은 화면이 정상으로 보이게 된다.
|
|
- generic [ref=f9e146]:
|
|
- button "위로" [ref=f9e147]
|
|
- button "아래로" [disabled] [ref=f9e148]
|
|
- button "삭제" [ref=f9e149]
|
|
- button "사실 추가" [ref=f9e150]
|
|
- group "가정" [ref=f9e151]:
|
|
- generic [ref=f9e153]:
|
|
- generic [ref=f9e154]:
|
|
- generic [ref=f9e155]: 가정 1
|
|
- textbox "가정 1" [ref=f9e156]: 운영에서는 replica가 둘 이상이고 저장소를 공유해 같은 authorized client 항목을 보게 된다.
|
|
- generic [ref=f9e157]:
|
|
- button "위로" [disabled] [ref=f9e158]
|
|
- button "아래로" [ref=f9e159]
|
|
- button "삭제" [ref=f9e160]
|
|
- generic [ref=f9e161]:
|
|
- generic [ref=f9e162]:
|
|
- generic [ref=f9e163]: 가정 2
|
|
- textbox "가정 2" [ref=f9e164]: 두 replica가 비슷한 시각에 만료를 만나면 각각 갱신을 시도하게 된다.
|
|
- generic [ref=f9e165]:
|
|
- button "위로" [ref=f9e166]
|
|
- button "아래로" [disabled] [ref=f9e167]
|
|
- button "삭제" [ref=f9e168]
|
|
- button "가정 추가" [ref=f9e169]
|
|
- group "미지수" [ref=f9e170]:
|
|
- generic [ref=f9e172]:
|
|
- generic [ref=f9e173]:
|
|
- generic [ref=f9e174]: 미지수 1
|
|
- textbox "미지수 1" [ref=f9e175]: 같은 refresh token으로 두 replica가 동시에 갱신하면 어느 쪽이 이기고 지는 쪽은 무엇을 받게 되는가.
|
|
- generic [ref=f9e176]:
|
|
- button "위로" [disabled] [ref=f9e177]
|
|
- button "아래로" [ref=f9e178]
|
|
- button "삭제" [ref=f9e179]
|
|
- generic [ref=f9e180]:
|
|
- generic [ref=f9e181]:
|
|
- generic [ref=f9e182]: 미지수 2
|
|
- textbox "미지수 2" [ref=f9e183]: 재사용 허용 0회에서 지는 쪽의 요청이 사용자 화면에 어떻게 보이게 되는가. 로그인 만료로 보이는가 일시적 오류로 보이는가.
|
|
- generic [ref=f9e184]:
|
|
- button "위로" [ref=f9e185]
|
|
- button "아래로" [ref=f9e186]
|
|
- button "삭제" [ref=f9e187]
|
|
- generic [ref=f9e188]:
|
|
- generic [ref=f9e189]:
|
|
- generic [ref=f9e190]: 미지수 3
|
|
- textbox "미지수 3" [ref=f9e191]: 지는 쪽이 저장소에서 새 token을 다시 읽어 재시도하면 성공하게 되는가, 아니면 재인증이 필요해지는가.
|
|
- generic [ref=f9e192]:
|
|
- button "위로" [ref=f9e193]
|
|
- button "아래로" [ref=f9e194]
|
|
- button "삭제" [ref=f9e195]
|
|
- generic [ref=f9e196]:
|
|
- generic [ref=f9e197]:
|
|
- generic [ref=f9e198]: 미지수 4
|
|
- textbox "미지수 4" [ref=f9e199]: 갱신을 한 곳에서만 할 것인가, 각자 하게 두고 실패는 재시도로 처리할 것인가.
|
|
- generic [ref=f9e200]:
|
|
- button "위로" [ref=f9e201]
|
|
- button "아래로" [ref=f9e202]
|
|
- button "삭제" [ref=f9e203]
|
|
- generic [ref=f9e204]:
|
|
- generic [ref=f9e205]:
|
|
- generic [ref=f9e206]: 미지수 5
|
|
- textbox "미지수 5" [ref=f9e207]: lock을 쓴다면 어디에 두고 얼마나 잡게 되는가. 잡은 채로 프로세스가 내려가면 어떻게 푸는가.
|
|
- generic [ref=f9e208]:
|
|
- button "위로" [ref=f9e209]
|
|
- button "아래로" [ref=f9e210]
|
|
- button "삭제" [ref=f9e211]
|
|
- generic [ref=f9e212]:
|
|
- generic [ref=f9e213]:
|
|
- generic [ref=f9e214]: 미지수 6
|
|
- textbox "미지수 6" [ref=f9e215]: 갱신 실패를 로그인 만료와 구분해서 표시할 수 있게 되는가.
|
|
- generic [ref=f9e216]:
|
|
- button "위로" [ref=f9e217]
|
|
- button "아래로" [disabled] [ref=f9e218]
|
|
- button "삭제" [ref=f9e219]
|
|
- button "미지수 추가" [ref=f9e220]
|
|
- group "제약" [ref=f9e221]:
|
|
- generic [ref=f9e223]:
|
|
- generic [ref=f9e224]:
|
|
- generic [ref=f9e225]: 제약 1
|
|
- textbox "제약 1" [ref=f9e226]: rotation과 재사용 0회는 이미 realm 설정이라서 이 전제를 바꾸지 않고 답해야 한다.
|
|
- generic [ref=f9e227]:
|
|
- button "위로" [disabled] [ref=f9e228]
|
|
- button "아래로" [ref=f9e229]
|
|
- button "삭제" [ref=f9e230]
|
|
- generic [ref=f9e231]:
|
|
- generic [ref=f9e232]:
|
|
- generic [ref=f9e233]: 제약 2
|
|
- textbox "제약 2" [ref=f9e234]: 이미 발급된 access token은 만료 전까지 사용할 수 있으므로 refresh 실패는 즉시 보이지 않을 수 있다. 재현 테스트는 access token 만료 직후에 맞춰 실행한다.
|
|
- generic [ref=f9e235]:
|
|
- button "위로" [ref=f9e236]
|
|
- button "아래로" [ref=f9e237]
|
|
- button "삭제" [ref=f9e238]
|
|
- generic [ref=f9e239]:
|
|
- generic [ref=f9e240]:
|
|
- generic [ref=f9e241]: 제약 3
|
|
- textbox "제약 3" [ref=f9e242]: 이 경쟁은 저장소를 공유한 뒤에야 재현되기 때문에 저장소 결정이 이 질문보다 앞서게 된다.
|
|
- generic [ref=f9e243]:
|
|
- button "위로" [ref=f9e244]
|
|
- button "아래로" [disabled] [ref=f9e245]
|
|
- button "삭제" [ref=f9e246]
|
|
- button "제약 추가" [ref=f9e247]
|
|
- group "선택지" [ref=f9e248]:
|
|
- generic [ref=f9e250]:
|
|
- generic [ref=f9e251]:
|
|
- generic [ref=f9e252]: 선택지 1 제목
|
|
- textbox "선택지 1 제목" [ref=f9e253]: 분산 lock으로 갱신을 직렬화한다
|
|
- generic [ref=f9e254]:
|
|
- generic [ref=f9e255]: 선택지 1 설명
|
|
- textbox "선택지 1 설명" [ref=f9e256]: 한 replica만 갱신하고 나머지는 끝나기를 기다렸다가 결과를 읽게 되어서 재사용 거부가 아예 생기지 않는다. 분산 lock을 사용하면 refresh 구간을 직렬화할 수 있다. lock 저장소의 가용성, lock 만료, 재진입, lock 보유 process 종료 상황까지 함께 처리해야 한다.
|
|
- generic [ref=f9e257]:
|
|
- button "위로" [disabled] [ref=f9e258]
|
|
- button "아래로" [ref=f9e259]
|
|
- button "삭제" [ref=f9e260]
|
|
- generic [ref=f9e261]:
|
|
- generic [ref=f9e262]:
|
|
- generic [ref=f9e263]: 선택지 2 제목
|
|
- textbox "선택지 2 제목" [ref=f9e264]: 각자 갱신하고 실패는 재시도로 처리한다
|
|
- generic [ref=f9e265]:
|
|
- generic [ref=f9e266]: 선택지 2 설명
|
|
- textbox "선택지 2 설명" [ref=f9e267]: 구현이 가장 단순하다. 지는 쪽이 거부를 받으면 저장소에서 최신 token을 다시 읽어 재시도한다는 전제인데, 이 재시도가 성립하는지는 아직 확인하지 않았다. reuse detection 정책에 따라 같은 refresh token의 두 번째 사용이 token family 전체에 영향을 줄 수 있다. 이 경우 단순 retry로 끝나지 않고 재인증이 필요할 수 있다. 갱신에 성공한 replica가 새 token을 저장하기 전에 다른 replica가 다시 조회하는 순서도 별도로 재현해야 한다.
|
|
- generic [ref=f9e268]:
|
|
- button "위로" [ref=f9e269]
|
|
- button "아래로" [ref=f9e270]
|
|
- button "삭제" [ref=f9e271]
|
|
- generic [ref=f9e272]:
|
|
- generic [ref=f9e273]:
|
|
- generic [ref=f9e274]: 선택지 3 제목
|
|
- textbox "선택지 3 제목" [ref=f9e275]: 갱신 전용 경로를 하나 둔다
|
|
- generic [ref=f9e276]:
|
|
- generic [ref=f9e277]: 선택지 3 설명
|
|
- textbox "선택지 3 설명" [ref=f9e278]: refresh를 전담하는 구성요소 하나만 refresh token을 사용하고 다른 replica는 갱신 결과를 조회하도록 구성할 수 있다. refresh 전담 구성요소가 중단되면 access token 만료 이후 갱신을 수행할 주체가 없어지므로 해당 구성요소의 가용성과 복구 방식이 중요해진다.
|
|
- generic [ref=f9e279]:
|
|
- button "위로" [ref=f9e280]
|
|
- button "아래로" [ref=f9e281]
|
|
- button "삭제" [ref=f9e282]
|
|
- generic [ref=f9e283]:
|
|
- generic [ref=f9e284]:
|
|
- generic [ref=f9e285]: 선택지 4 제목
|
|
- textbox "선택지 4 제목" [ref=f9e286]: 제약상 제외 — 재사용 허용을 늘린다
|
|
- generic [ref=f9e287]:
|
|
- generic [ref=f9e288]: 선택지 4 설명
|
|
- textbox "선택지 4 설명" [ref=f9e289]: 짧은 유예를 주면 경쟁이 저절로 해소되고 코드도 고칠 필요가 없다. 다만 rotation과 재사용 0회는 이 질문이 바꾸지 않기로 한 realm 설정이다. 훔친 refresh token을 쓸 수 있는 창도 같이 늘어난다. 비교 대상으로만 남긴다.
|
|
- generic [ref=f9e290]:
|
|
- button "위로" [ref=f9e291]
|
|
- button "아래로" [disabled] [ref=f9e292]
|
|
- button "삭제" [ref=f9e293]
|
|
- button "선택지 추가" [ref=f9e294]
|
|
- generic [ref=f9e295]:
|
|
- generic [ref=f9e296]: 다음 검증
|
|
- textbox "다음 검증" [ref=f9e297]: 저장소를 공유한 뒤에 재현한다. 1. replica 두 대에서 같은 사용자로 access token 만료 직후 동시에 요청을 보낸다. 2. 이긴 쪽과 지는 쪽의 응답을 각각 기록한다. 3. 지는 쪽이 저장된 새 token으로 재시도해 성공하는지 본다. 4. 지는 쪽 사용자 화면에 무엇이 보이는지 기록한다. 5. lock을 넣은 구성과 안 넣은 구성을 같은 입력으로 비교해 실패율과 지연을 잰다. 실패가 사용자에게 노출되면 lock을 고르고, 노출되지 않으면 재시도로 둔다.
|
|
- region [ref=f9e298]:
|
|
- generic [ref=f9e299]:
|
|
- paragraph [ref=f9e300]: LIVE
|
|
- heading "즉시 미리보기" [level=2] [ref=f9e301]
|
|
- generic [ref=f9e304]:
|
|
- generic [ref=f9e305]:
|
|
- navigation "문서 경로" [ref=f9e306]:
|
|
- link "Open Question" [ref=f9e307] [cursor=pointer]:
|
|
- /url: /explore/questions
|
|
- generic [ref=f9e308]: /
|
|
- generic [ref=f9e309]: OAuth/OIDC 인증 경계
|
|
- generic [ref=f9e310]: /
|
|
- link "KeyCloak Patterns" [ref=f9e311] [cursor=pointer]:
|
|
- /url: /projects/keycloak-patterns
|
|
- heading "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" [level=1] [ref=f9e312]
|
|
- paragraph [ref=f9e313]: realm이 refresh token rotation과 재사용 허용 0회를 쓴다. 두 replica가 같은 refresh token으로 동시에 갱신할 수 있고, 그때 두 번째 사용이 거부될 가능성이 있다. 실제 Keycloak 응답과 session 영향은 아직 재현하지 않았다.
|
|
- generic [ref=f9e314]:
|
|
- generic [ref=f9e315]:
|
|
- term [ref=f9e316]: 유형
|
|
- definition [ref=f9e317]: Open Question
|
|
- generic [ref=f9e318]:
|
|
- term [ref=f9e319]: 프로젝트
|
|
- definition [ref=f9e320]: KeyCloak Patterns
|
|
- generic [ref=f9e321]:
|
|
- term [ref=f9e322]: 게시
|
|
- definition [ref=f9e323]: 게시 전
|
|
- paragraph [ref=f9e324]: OPEN
|
|
- article [ref=f9e325]:
|
|
- region [ref=f9e326]:
|
|
- heading "확인한 사실" [level=2] [ref=f9e327]
|
|
- list [ref=f9e328]:
|
|
- listitem [ref=f9e329]: realm은 refresh token rotation과 재사용 허용 0회를 쓰게 되어서, 한 번 갱신하면 이전 refresh token은 바로 무효가 된다.
|
|
- listitem [ref=f9e330]: 커밋된 테스트는 새 refresh token 발급과 이전 token 거부, revocation 뒤 refresh 실패를 확인하는데 모두 한 주체가 순서대로 부르는 경우다.
|
|
- listitem [ref=f9e331]: authorized client manager에는 refresh-token provider가 구성되어 있어 access token 만료 시 refresh를 시도할 수 있다.
|
|
- listitem [ref=f9e332]: 다만 만료를 기다려 실제 갱신이 성공하고 새 token이 저장되는지까지는 확인하지 않았다.
|
|
- listitem [ref=f9e333]: 현재 authorized client 저장소는 process-local이라 replica가 같은 refresh token 상태를 공유하지 않는다. 따라서 이번 단일 인스턴스 검증에서는 동시 refresh 경쟁을 재현하지 않았다.
|
|
- listitem [ref=f9e334]: 이미 발급된 access token은 만료 전까지 API에서 계속 통하기 때문에, 갱신이 실패해도 그동안은 화면이 정상으로 보이게 된다.
|
|
- region [ref=f9e335]:
|
|
- heading "가정" [level=2] [ref=f9e336]
|
|
- list [ref=f9e337]:
|
|
- listitem [ref=f9e338]: 운영에서는 replica가 둘 이상이고 저장소를 공유해 같은 authorized client 항목을 보게 된다.
|
|
- listitem [ref=f9e339]: 두 replica가 비슷한 시각에 만료를 만나면 각각 갱신을 시도하게 된다.
|
|
- region [ref=f9e340]:
|
|
- heading "남은 미지수" [level=2] [ref=f9e341]
|
|
- list [ref=f9e342]:
|
|
- listitem [ref=f9e343]: 같은 refresh token으로 두 replica가 동시에 갱신하면 어느 쪽이 이기고 지는 쪽은 무엇을 받게 되는가.
|
|
- listitem [ref=f9e344]: 재사용 허용 0회에서 지는 쪽의 요청이 사용자 화면에 어떻게 보이게 되는가. 로그인 만료로 보이는가 일시적 오류로 보이는가.
|
|
- listitem [ref=f9e345]: 지는 쪽이 저장소에서 새 token을 다시 읽어 재시도하면 성공하게 되는가, 아니면 재인증이 필요해지는가.
|
|
- listitem [ref=f9e346]: 갱신을 한 곳에서만 할 것인가, 각자 하게 두고 실패는 재시도로 처리할 것인가.
|
|
- listitem [ref=f9e347]: lock을 쓴다면 어디에 두고 얼마나 잡게 되는가. 잡은 채로 프로세스가 내려가면 어떻게 푸는가.
|
|
- listitem [ref=f9e348]: 갱신 실패를 로그인 만료와 구분해서 표시할 수 있게 되는가.
|
|
- region [ref=f9e349]:
|
|
- heading "제약" [level=2] [ref=f9e350]
|
|
- list [ref=f9e351]:
|
|
- listitem [ref=f9e352]: rotation과 재사용 0회는 이미 realm 설정이라서 이 전제를 바꾸지 않고 답해야 한다.
|
|
- listitem [ref=f9e353]: 이미 발급된 access token은 만료 전까지 사용할 수 있으므로 refresh 실패는 즉시 보이지 않을 수 있다. 재현 테스트는 access token 만료 직후에 맞춰 실행한다.
|
|
- listitem [ref=f9e354]: 이 경쟁은 저장소를 공유한 뒤에야 재현되기 때문에 저장소 결정이 이 질문보다 앞서게 된다.
|
|
- region [ref=f9e355]:
|
|
- heading "검토한 선택지" [level=2] [ref=f9e356]
|
|
- list [ref=f9e357]:
|
|
- listitem [ref=f9e358]:
|
|
- heading "분산 lock으로 갱신을 직렬화한다" [level=3] [ref=f9e359]
|
|
- paragraph [ref=f9e360]: 한 replica만 갱신하고 나머지는 끝나기를 기다렸다가 결과를 읽게 되어서 재사용 거부가 아예 생기지 않는다. 분산 lock을 사용하면 refresh 구간을 직렬화할 수 있다. lock 저장소의 가용성, lock 만료, 재진입, lock 보유 process 종료 상황까지 함께 처리해야 한다.
|
|
- listitem [ref=f9e361]:
|
|
- heading "각자 갱신하고 실패는 재시도로 처리한다" [level=3] [ref=f9e362]
|
|
- paragraph [ref=f9e363]: 구현이 가장 단순하다. 지는 쪽이 거부를 받으면 저장소에서 최신 token을 다시 읽어 재시도한다는 전제인데, 이 재시도가 성립하는지는 아직 확인하지 않았다. reuse detection 정책에 따라 같은 refresh token의 두 번째 사용이 token family 전체에 영향을 줄 수 있다. 이 경우 단순 retry로 끝나지 않고 재인증이 필요할 수 있다. 갱신에 성공한 replica가 새 token을 저장하기 전에 다른 replica가 다시 조회하는 순서도 별도로 재현해야 한다.
|
|
- listitem [ref=f9e364]:
|
|
- heading "갱신 전용 경로를 하나 둔다" [level=3] [ref=f9e365]
|
|
- paragraph [ref=f9e366]: refresh를 전담하는 구성요소 하나만 refresh token을 사용하고 다른 replica는 갱신 결과를 조회하도록 구성할 수 있다. refresh 전담 구성요소가 중단되면 access token 만료 이후 갱신을 수행할 주체가 없어지므로 해당 구성요소의 가용성과 복구 방식이 중요해진다.
|
|
- listitem [ref=f9e367]:
|
|
- heading "제약상 제외 — 재사용 허용을 늘린다" [level=3] [ref=f9e368]
|
|
- paragraph [ref=f9e369]: 짧은 유예를 주면 경쟁이 저절로 해소되고 코드도 고칠 필요가 없다. 다만 rotation과 재사용 0회는 이 질문이 바꾸지 않기로 한 realm 설정이다. 훔친 refresh token을 쓸 수 있는 창도 같이 늘어난다. 비교 대상으로만 남긴다.
|
|
- region [ref=f9e370]:
|
|
- paragraph [ref=f9e371]: Next
|
|
- heading "다음 검증" [level=2] [ref=f9e372]
|
|
- paragraph [ref=f9e373]: 저장소를 공유한 뒤에 재현한다.1. replica 두 대에서 같은 사용자로 access token 만료 직후 동시에 요청을 보낸다.2. 이긴 쪽과 지는 쪽의 응답을 각각 기록한다.3. 지는 쪽이 저장된 새 token으로 재시도해 성공하는지 본다.4. 지는 쪽 사용자 화면에 무엇이 보이는지 기록한다.5. lock을 넣은 구성과 안 넣은 구성을 같은 입력으로 비교해 실패율과 지연을 잰다.실패가 사용자에게 노출되면 lock을 고르고, 노출되지 않으면 재시도로 둔다.
|
|
- region [ref=f9e374]:
|
|
- paragraph [ref=f9e375]: Relations
|
|
- heading "이 기록과 연결된 맥락" [level=2] [ref=f9e376]
|
|
- list [ref=f9e377]:
|
|
- listitem [ref=f9e378]:
|
|
- link "rotation과 재사용 0회를 쓰는 구성의 출처다. Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f9e379] [cursor=pointer]:
|
|
- /url: /cases/split-custody-access-token
|
|
- generic [ref=f9e380]: rotation과 재사용 0회를 쓰는 구성의 출처다.
|
|
- strong [ref=f9e381]: Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출
|
|
- generic [ref=f9e382]: ↗
|
|
- complementary [ref=f9e383]:
|
|
- heading "작업 상태" [level=2] [ref=f9e384]
|
|
- status "편집 상태" [ref=f9e385]: 저장됨
|
|
- generic [ref=f9e386]:
|
|
- generic [ref=f9e387]:
|
|
- term [ref=f9e388]: 저장 버전
|
|
- definition [ref=f9e389]: "11"
|
|
- generic [ref=f9e390]:
|
|
- term [ref=f9e391]: 종류
|
|
- definition [ref=f9e392]: QUESTION
|
|
- paragraph [ref=f9e393]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
|
|
- generic [ref=f9e394]:
|
|
- button "저장" [disabled] [ref=f9e395]
|
|
- button "게시" [ref=f9e396]
|
|
- paragraph [ref=f9e397]: 버전 11으로 저장했습니다. |