통합과 배포 · 만료는 사고가 아니다 · 이론
토큰은 반드시 만료한다
한 줄 요약
접근 토큰은 짧게 살다 죽는 것이 정상이므로, 클라이언트는 만료 전에 미리 갈고, 시계 오차만큼 여유를 두고, 401 을 받으면 딱 한 번 갱신해 재시도하고, 워커가 여럿이어도 갱신은 한 번만 일어나게 해야 한다.
왜 이게 필요했나
"야간 배치가 매번 두세 건씩 401 로 실패합니다" 는 신고를 따라가면 원인은 거의 언제나 같다. 토큰을 한 번 받아 두고, 401 이 오면 그때 새로 받는 구조다. 이 구조는 만료 순간마다 반드시 한 번은 실패한다. 그 실패가 사용자 요청이면 사용자가 본다.
토큰이 짧게 사는 것은 결함이 아니라 설계다. [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749.html)는 접근 토큰의 수명을 짧게 두고 갱신 토큰으로 다시 받는 구조를 정의하고, [RFC 6750](https://www.rfc-editor.org/rfc/rfc6750.html)은 그 토큰을 Authorization: Bearer 로 실어 보내는 방법과 실패했을 때의 WWW-Authenticate 응답을 정한다. 수명이 짧아야 토큰이 새어 나갔을 때 쓸 수 있는 기간이 짧아진다 — 그것이 목적이다.
어떻게 동작하나
토큰 안을 읽는 것과 믿는 것은 다르다. JWT([RFC 7519](https://www.rfc-editor.org/rfc/rfc7519.html))는 점으로 나뉜 세 조각이고, 앞의 두 조각은 암호가 아니라 base64url 로 인코딩된 평문이다. 누구나 열어 볼 수 있고 누구나 고칠 수 있다. 그래서 클라이언트가 exp 를 읽어 "언제 갈지" 를 정하는 것은 괜찮지만, 그 값을 근거로 권한을 판단하면 안 된다. 서명 검증은 자원 서버의 일이다.
claim 중 시간에 관한 것은 셋이다. iat(발급 시각), nbf(이 시각 전에는 쓰지 말 것), exp(이 시각 이후에는 쓰지 말 것). RFC 7519 는 exp 와 nbf 를 다룰 때 작은 시계 오차를 감안하는 여유(leeway)를 두어도 좋다고 적는다. 서버와 클라이언트의 시계가 몇 초 어긋나는 것은 흔하고, 여유가 없으면 그 몇 초 때문에 방금 받은 토큰이 "아직 유효하지 않음" 으로 거절된다.
갱신은 만료 전에 한다. 규칙은 한 줄이다. 남은 시간 <= 여유 면 지금 간다. 여유를 크게 잡으면 갱신이 잦아지고, 작게 잡으면 만료 순간에 걸릴 위험이 커진다. 수명의 10%에서 20% 사이가 흔한 출발점이다.
401 은 한 번만 받는다. 미리 갈아도 401 은 올 수 있다 — 서버가 토큰을 미리 폐기했거나, 권한이 바뀌었거나, 시계가 크게 어긋났을 때다. 이때 갱신하고 딱 한 번 다시 건다. 여기서 횟수 제한을 안 두면 고리가 생긴다. 토큰이 아니라 권한이 문제인 경우(사실은 403 이어야 할 상황을 401 로 답하는 서버가 많다) 갱신해도 계속 401 이고, 클라이언트는 무한히 토큰을 받아 가며 인증 서버를 때린다.
갱신 폭주(stampede)를 막는다. 워커 20개가 같은 토큰을 공유하면 만료 순간에 20개가 동시에 "갱신해야 한다" 고 판단한다. 인증 서버 입장에서는 평소의 20배 요청이 한꺼번에 오는 것이고, 그 서버가 느려지면 갱신이 더 오래 걸려 더 많은 워커가 몰린다. 해법은 갱신을 한 번만 하게 만드는 것이다.
토큰 필요 │ ├─ 캐시를 본다 ── 아직 쓸 만한가? ── 예 ─▶ 그대로 쓴다 (대부분 여기서 끝난다) │ 아니오 └─ 잠금을 잡는다 ──▶ 캐시를 **다시** 본다 ── 남이 갈아 뒀는가? ── 예 ─▶ 그것을 쓴다 아니오 ─▶ 내가 갱신한다잠금을 잡은 뒤에 한 번 더 확인하는 것이 핵심이다. 기다리는 사이에 다른 워커가 이미 갈아 두었을 수 있기 때문이다. 이 한 줄을 빼면 잠금이 순서만 줄 세울 뿐 갱신 횟수는 그대로다.
현장에서 만나는 모습
첫째, 401 과 403 을 섞어 쓰는 서버가 많다. 권한이 모자란 상황에 401 을 주면 클라이언트는 "토큰이 문제구나" 하고 갱신을 반복한다. 우리 쪽에서는 재시도 횟수를 1로 묶는 것이 유일한 방어다.
둘째, 토큰을 프로세스 메모리에만 둔다. 워커가 프로세스 단위로 흩어져 있으면 캐시를 공유하지 못해 프로세스 수만큼 발급이 일어난다. 발급 횟수에 제한이 있는 파트너라면 그 자체로 장애가 된다.
셋째, 시계를 안 맞춘다. 컨테이너의 시계가 30초 어긋나 있으면 여유를 아무리 잘 잡아도 어긋난 쪽으로 밀린다. 401 이 특정 노드에서만 난다면 그 노드의 시계를 먼저 본다.
넷째, 토큰이 로그에 남는다. 접근 토큰은 그 자체가 자격 증명이다. 요청 헤더를 통째로 찍는 디버그 로그가 남아 있으면 그 로그를 읽을 수 있는 사람 모두가 그 API 를 부를 수 있다.
다음 실습에서 할 것
짧은 수명의 토큰을 내주는 인증 서버를 띄워 토큰을 하나 받고, 그 안을 열어 iat·nbf·exp 를 읽는다. 만료·미도래·임박·정상 네 갈래를 시계 오차 여유까지 감안해 가려내는 판정기를 만들고, 그 판정으로 만료 전에 미리 갱신하며 계속 호출하는 클라이언트를 만든다. 그다음 무엇을 들고 가도 401 을 주는 서버에 대고 재시도가 한 번에서 멈추는지 확인하고, 워커 여섯이 동시에 시작해도 발급이 한 번만 일어나게 만든다. 마지막으로 이 값들을 정책 표로 적는다.