You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[P2] [서버 연동] refresh 로 받은 새 토큰을 앱이 쓰지 못해 몇 시간 뒤 옛 토큰으로 갱신하다 강제 로그아웃된다 (SEC-05) #659
#642(동시 refresh) 를 고친 뒤에도 prod 에서 강제 로그아웃이 하루 3~5건씩 계속됐습니다(서버 경보 SEC-05). 이번 모양은 동시 호출 경합이 아닙니다.
앱이 refresh 로 받은 새 토큰을 쓰지 못한 채, 몇 분~몇 시간 뒤 옛 refreshToken 으로 다시 refresh 합니다.
prod 에서 확인한 것 (10/29 ~ 11/02, 23건 · 20명)
서버 DB(refresh_tokens)와 요청 로그로 확인한 사실입니다.
항목
값
회전(정상 refresh) 뒤 옛 RT 가 다시 온 간격
5분 ~ 11시간 (1시간 안 10건, 6시간 안 16건, 12시간 안 23건 전부)
회전으로 내려준 새 RT 가 한 번도 refresh 에 쓰이지 않은 경우
23건 중 21건
같은 사용자의 다른 기기 세션
0건 (탈취 흔적 없음)
로그아웃된 뒤 다시 로그인한 사람
3명뿐
다섯 건은 요청 순서를 직접 따라가 봤습니다.
만료된 AT 로 GET /api/v1/challenges?filter=IN_PROGRESS 와 POST /api/v1/verifications/intro 가 거의 동시에 나가고 둘 다 401 입니다. 매번 이 두 호출입니다.
POST /api/v1/auth/refresh 를 한 번 보내고 서버는 200 으로 새 페어를 내려줍니다(서버에서는 회전 완료).
그 뒤 앱의 동작은 경우마다 달랐습니다.
3건은 새 AT 로 나간 요청이 하나도 없습니다. 새 토큰을 받지 못했거나 버린 것으로 보입니다.
1건은 새 AT 로 요청을 계속 보냈습니다. 그런데 몇 시간 뒤 refresh 에는 옛 RT 를 냈습니다. 새 RT 가 저장되지 않은 것으로 보입니다.
몇 분~몇 시간 뒤 1과 같은 두 호출이 다시 401 을 받고, 옛 RT 로 refresh 를 보냅니다. 서버는 이미 회전된 토큰의 재사용으로 보고 세션을 폐기했고(지금은 아래 「서버 쪽」 참고), 앱은 로그인 화면으로 갑니다.
앱에서 확인해 주실 것
원인은 서버에서 볼 수 없어서 아래는 의심 지점입니다.
1·4 의 두 호출(challenges?filter=IN_PROGRESS + verifications/intro)을 누가 부르는지 — 앱 진입인지 백그라운드 작업(WorkManager 등)인지. 백그라운드라면 refresh 응답을 받기 전에 작업이나 프로세스가 끝나는지 확인이 필요합니다.
refresh 200 응답을 받고도 저장하지 못하는 경로
TokenRepositoryImpl.write() 는 IOException 을 로그만 남기고 삼킵니다. saveTokens 는 cachedAccess 를 먼저 바꾼 뒤 쓰기 때문에, 쓰기가 실패하면 「메모리엔 새 AT, 디스크엔 옛 RT」가 됩니다(위 3의 두 번째 경우와 같은 모양).
refresh 경로(saveTokensIfRefreshTokenMatches)가 false 를 돌려주는 경우에 새 페어가 버려지는지 확인 부탁드립니다.
위 분기마다 앱 로그(Observability)를 남겨 주시면 서버 로그와 맞춰 볼 수 있습니다.
refresh 시도 / 200 수신 / 저장 성공·실패 / 예외 종류
앱 버전
서버 쪽 (확정된 내용)
11/02 19:32 KST prod 반영(Backend test(verification): 장소 검색·인트로 정책 매핑 #396): 회전 유예를 30초 → 12시간으로 늘렸습니다. 회전된 RT 가 12시간 안에 다시 오고, 그 회전으로 내려준 새 RT 가 아직 refresh 에 쓰이지 않았으면 세션을 끊지 않고 새 페어를 한 번 더 줍니다. 응답 형식은 같습니다.
그래서 위 사례는 지금 로그아웃되지 않습니다. 다만 이건 완충일 뿐입니다. 12시간을 넘기거나 새 RT 로 이미 refresh 한 뒤의 옛 RT 는 지금처럼 세션이 폐기됩니다. 이 창은 보안상 넓은 편이라 앱이 고쳐지면 다시 줄일 예정입니다.
완료 기준
refresh 200 을 받은 뒤 다음 refresh 에 항상 그 응답의 새 RT 를 쓴다.
서버 지표 RefreshGraceReissueCount(유예로 재발급한 횟수)가 새 버전 보급 뒤 0 에 가까워진다. 서버가 이 지표를 보고 유예를 줄입니다.
Activity
selee1012 commentedon Nov 4, 2026
추가 사례: 12시간 유예 밖 2건 (11/03, 11/04)
12시간 유예(Backend #396)를 prod 에 반영한 뒤에도 유예 창 밖 재사용이 하루 1건씩 나와 세션이 끊겼습니다. 두 건 모두 본문의 「새 AT 로 나간 요청이 하나도 없다」 경우와 같습니다.
GET /challenges?filter=IN_PROGRESS401 (이번에는intro없이 이 호출 하나)GET /challenges401 직후)참고로 같은 기간에 유예 덕분에 로그아웃을 피한 경우(
RefreshGraceReissueCount)는 11/03 8건, 11/04(오전까지) 3건입니다. 응답을 잃는 일 자체는 하루 10건 안팎으로 계속 일어나고 있습니다.이번 사례로 좁혀지는 것
17시간 뒤 다시 깨어난 작업은 디스크에 남은 옛 RT 를 읽었습니다. 그래서 refresh 200 응답이 앱에 도착했더라도 새 페어가 디스크에 저장되기 전에 흐름이 끊긴 것으로 보입니다. 본문 의심 지점 가운데 아래를 먼저 봐 주시면 좋겠습니다.
saveTokens…를 마치기 전에 코루틴이 취소되거나 프로세스가 종료되면, 서버에서는 회전이 끝났는데 앱 디스크에는 옛 RT 가 남습니다.NonCancellable등)으로 묶고, 저장이 끝난 뒤에 작업을 끝내 주세요.TokenRepositoryImpl.write()가IOException을 삼키는 경로 (본문과 같음).서버 쪽 계획은 바뀌지 않았습니다. 유예는 12시간으로 유지합니다. 앱 수정이 보급되어
RefreshGraceReissueCount가 0 에 가까워지면 줄입니다.