현장 장비 에이전트의 인증을 JWT 로 바꾸고 잘 돌다가 한 시간 뒤에 요청이 전부 401 로 실패했다. 한 번 받은 토큰을 계속 쓰고 있었고 그 토큰에는 exp 가 들어 있었다.
Table of contents
Open Table of contents
원인 — 만료를 안 다룬 첫 구현
로그인해서 받은 값을 Authorization: Bearer 헤더에 넣고 그대로 두는 코드였다. 토큰의 페이로드를 열어 보니 iat 와 exp 가 있었다.
RFC 7519 는 exp 이후의 토큰을 처리에 받아들이면 안 된다고 정한다. 인증이 성공했다는 것과 그 인증이 계속 유효하다는 것은 다르다. 토큰 방식으로 옮기면서 그 차이를 코드에 반영하지 않은 것이 원인이었다.
해 본 것 — 401을 받고 다시 하는 구조
401 을 받으면 다시 로그인하고 한 번 더 보내게 고쳤더니 돌아가기는 했다. 다만 만료 때마다 반드시 한 번은 실패하고 다시 하는 구조였다.
동시에 여러 요청이 나가면 전부 그 응답을 받고 전부 각자 다시 로그인한다. 실패를 신호로 삼는 구조는 동시성이 붙으면 그 신호가 배로 증폭된다.
조치 — 미리 갱신하고 동시 갱신을 막는다
만료 조금 전에 미리 새 토큰을 받는 쪽을 골랐다. 이유는 401 을 아예 안 만나는 것이 실패 뒤 재시도보다 단순해서다. 갱신 구간에 synchronized 잠금을 걸어 여러 스레드가 동시에 로그인하지 않게 했다.
잠금을 안 걸면 만료 직후에 로그인 요청이 한꺼번에 몰린다. 대신 갱신 타이머가 도는 동안은 토큰을 한 번 더 받는 요청이 늘어나는 것을 감수했다. 실패한 뒤에 반응하는 대신 만료되기 전에 움직이는 것이 이 문제의 형태에 맞는 대응이었다.
막힌 지점 — 가정한 만료와 받은 만료
처음에는 만료를 한 시간으로 가정해서 3600 을 코드에 넣었다. 서버가 만료 시간을 바꾸면 그 순간부터 안 맞는다.
응답에 expires_in 이 들어 있어서 그것을 쓰고 없으면 페이로드의 exp 에서 뽑아 쓰게 했다. 장비 시계와 서버 시계가 다를 수 있으므로 만료 판단에 여유를 뒀는데 RFC 7519 도 시계 차이를 위해 몇 분 이내의 여유를 둘 수 있다고 적는다.
주의 — 갱신 실패의 두 종류
갱신 자체가 실패할 때를 자격 증명 오류와 일시 오류로 나눴다. 앞의 것은 재시도해도 같은 결과이므로 바로 중단하고 알린다.
자격 증명이 틀린 채로 계속 재시도하면 계정이 잠기는데 실제로 그런 일이 있었다. refresh_token 을 쓸지는 어디서 도는지에 달렸고 장비 안의 에이전트라 비밀 값을 안전하게 둘 수 있어서 단순한 쪽으로 갔다.
디버그 로그가 헤더를 통째로 찍고 있어서 토큰이 로그에 남는 것도 그때 고쳤다. Authorization 헤더를 가리게 하고 지난 로그에 남은 것도 찾아 지웠다.
정리
- 토큰은 만료되므로 그때 무엇을 할지가 설계다
- 인증 성공과 인증의 지속은 다르다
- 실패를 신호로 삼으면 동시성이 붙을 때 신호가 증폭된다
- 만료 전에 미리 갱신하는 편이 낫다
- 여러 스레드가 동시에 갱신하지 않게 막는다
- 만료 시각을 가정하지 말고 응답이나 토큰에서 받는다
- 시계 차이를 감안해 여유를 둔다
- 갱신 실패에서 자격 증명 오류와 일시 오류를 나눈다