삽질기록 — 로그인 API 작성 (JWT, Refresh Token, Access Token, HTTP only)
지난 포스팅에서는 회원가입을 다뤄 보았고, 이번 포스팅에서는 로그인 API를 다루려고 한다. Flask 와 ORM 관련은 두 번째로 올리는 포스트기 때문에 설명이 부족할 수 있다. 혹시 추가로 궁금한 점이 있다면, 아래 포스트를 참고하길 바란다.
시작하기 전에, 백엔드 전문가가 볼 경우 어색한 부분이 있을 수 있습니다. 해당 부분을 알려 주신다면, 감사히 참고하도록 하겠습니다.
— — —
아래 블로그에서 JWT 관련 많은 참고를 했습니다. 감사합니다!
로그인 구현을 하면서 많은 글들을 찾아 보았다. 처음에는 단순하게 “가장 많이 이용하는 Token 방식을 이용하면 되는 거 아니야?” 했지만, 이번에는 왜 많은 사람들이 Token 방식을 이용하는 건지 궁금했다. 인증 구현 방식은 크게 두 가지가 있는데, 먼저 그 두 가지를 정리하고 시작하려고 한다.
1. 세션 방식
- 클라이언트에서 로그인을 요청한다.
- 서버는 세션을 만들어 세션 ID를 HTTP로 클라이언트에 보내 준다.
- 클라이언트는 세션 ID를 쿠키에 저장한다.
- 인증이 필요할 경우 쿠키의 세션 ID와 함께 데이터를 요청한다.
- 서버는 그 세션 ID를 검증한 후 인증을 완료한다.
- 정상일 경우 데이터를 넘겨 준다.
[장점]
- 세션 ID 변질 시 해당 세션을 삭제할 수 있다.
- 길이가 짧은 편이기 때문에, 브라우저에 저장하기 용이하다.
[단점]
- 추가 저장소가 필요하다. (세션 저장소)
- 로그인을 유지할 경우, 필요한 공간이 많아진다. (1번의 연장)
2. Token 방식 (JWT 등)
- 클라이언트에서 로그인 요청을 한다.
- 서버는 DB에서 사용자를 확인한 후, Token을 발급한다.
- 클라이언트는 Token을 안전한 곳에 저장한다.
- 클라이언트에서 Token과 함께 데이터를 요청한다.
- 서버는 Token을 검증한 후, 인증을 완료한다.
- 정상일 경우 데이터를 넘겨 준다.
[장점]
- Backend, Frontend 나누기에 편리하다.
- 확장성이 뛰어나다.
- 추가 저장소가 필요하지 않다.
[단점]
- JWT 안에 담을 수 있는 정보가 제한적이다. (payload에 있는 내용은 디코딩 할 경우 볼 수 있다.)
- 정보 탈취 가능성이 있다. (JWT는 토큰만 있다면 유효기간 안에 계속 사용할 수 있기 때문. 해당 토큰을 중간에 삭제할 수가 없다.)
위의 단점을 보완하고자 나온 것이 Refresh Token이다.
2–1. JWT 방식 (Access Token, Refresh Token)
- 클라이언트에서 로그인 요청을 한다.
- 서버는 DB에서 사용자를 확인한 후, Access Token과 Refresh Token을 발급한다.
- 클라이언트는 두 개의 토큰을 안전한 곳에 저장한다.
- 클라이언트에서 Access Token과 함께 데이터를 요청한다.
- 서버는 Token을 검증한 후, 인증을 완료한다.
- 정상일 경우 데이터를 넘겨 준다.
- 만료된 Access Token으로 데이터를 요청한다.
- 서버에서는 만료를 확인하고, 클라이언트에 보낸다.
- 클라이언트에서는 Refresh Token으로 서버에 Access Token 발급을 요청한다.
- Refresh Token 확인 후, 새로운 Access Token과 Refresh Token을 발급한다.
- …
[장점]
- Access Token만 있을 때보다 안전하다. (Access Token의 유효기간이 짧아지기 때문.)
[단점]
- 구현이 복잡하다.
- 안전한 저장소가 대체 어딘지…?
- 만약에 Refresh Token이 탈취되었을 경우에는 어떻게 해야 할지
…
서버의 확장성을 위해 추가 저장소가 필요하지 않은 Refresh Token, Access Token을 이용하기로 결정했다. 안전한 저장소와 Refresh Token이 탈취된 경우의 방안을 찾아야 할 텐데… 그건 이 게시글의 정리 부분에서 정리하도록 하겠다. (그것이 ‘정리’ 니까…)
+ JWT가 뭔지?
사실 JWT 설명은 공식 홈페이지 링크만 걸고 넘어가려 했으나, 나중의 나를 위해 짧게나마 설명을 남기려 한다.
JWT는 구분자를 .으로 사용하고, 세개의 문자열로 이뤄져있다.
헤더 (Header).내용 (Payload).서명 (Verify Signature)
- 헤더
- alg: 해싱 알고리즘이다. 보통 SHA256 혹은 RSA 가 사용되고, 이 알고리즘은 서명 부분에서 사용된다.
- typ: 토큰의 타입이다.
- 내용
- 토큰에 담을 정보가 들어있다. 보통 username과 유효기간을 넣는다.
- 디코딩하면 다른 사람도 확인할 수 있기 때문에, password 같은 예민한 개인정보는 넣으면 안 된다.
- 서명
- 헤더와 내용을 합친 후 Secret key로 해쉬하여 생성된다.
위에서 말한 것과 같이, Payload는 디코딩할 경우 다른 사람이 확인할 수 있다. 물론 Header도 동일하게 확인할 수 있다.
그럼 Payload의 정보 노출 가능성이 높아지지 않나?
맞다. 그래서 payload에 password과 같은 예민한 정보는 넣지 말라고 한 것이다.
하지만 토큰 조작은 불가하다. 서명 부분은 Secret key를 모를 경우에 복호화를 할 수 없기 때문이다.
만약 A가 로그인 한 후, B 사용자의 정보를 보기 위해 Payload의 username을 B로 바꿨다면, A의 username으로 해싱한 서명 부분에서 조작한 토큰으로 판단되어 오류를 리턴할 것이다.
그리고, 이미 만료된 토큰을 사용자 멋대로 갱신하기 위해 iat(유효기간)을 변경했을 때에도 조작된 토큰 오류가 리턴될 것이다.
위처럼 토큰 조작이 불가하기 때문에, 하나의 토큰으로 많은 정보가 노출될 가능성은 낮아진다.
— 정리
위에서 Refresh Token의 안전한 장소에 대해 언급했다. 실제로 이것을 찾는 데에 많은 삽질을 했다… 이 주제에는 많은 의견들이 있는 것 같다. (Web Storage, Cookie 등)
하지만, 필자는 서버보단 클라이언트에서 탈취될 가능성이 높다고 판단했기 때문에 모든 권한을 서버에 넘기기로 했다.
… 이게 무슨 말인가?
HTTP Only를 이용하기로 한 것이다.
HTTP Only는 자바스크립트의 document.cookie를 이용해서 쿠키에 접촉하는 것을 막는 옵션이다. 따라서 클라이언트에서 가져올 수도 없고, 바꿀 수도 없다.
또한, Refresh Token이 탈취되었을 경우에는 어떻게 해야 할지도 고민해 봐야 한다.
JWT는 유효기간 안에는 항상 사용할 수 있고, Refresh Token은 Access Token보다 유효기간을 길게 설정해야 한다.
여기서 문제가 된다. 만약 Refresh Token이 탈취된 것을 알았는데, 아무런 조치를 취하지 못하고 가만히 유효기간이 끝나길 기다려야 한다면… 엄청나게 속상할 것이다.
그래서 Refresh Token을 user DB에 저장하여 Access Token을 재발급할 때 실제로 존재하는 Refresh Token인지 확인하는 과정을 거치기로 했다. 그리고 만약, Refresh Token이 탈취되었음을 알아차릴 경우, 그 Refresh Token을 DB에서 삭제해 주면 된다.
그리하여 내가 원하는 것은, 아래와 같다.
- JWT 이용
- Refresh Token, Access Token 이용
- Refresh Token은 HTTP Only로, Access Token은 프로젝트의 안전한 저장소에 저장.
쓰다보니 내용이 너무 길어져서… 실제 코딩은 다음 포스트에서 하겠다.