jwt_homepage

🔥JWT


JWT : Json Web Token

JWT는 당사자 간에 JSON 객체로 정보를 안전하게 전송할 수 있는 개방형 표준이다. JWT는 API를 사용하는 클라이언트를 인증식별하는데 사용할 수 있어서 보통 RESTful API에서 사용한다.

🔥인증 vs 인가


JWT를 설명하면서 인증인가에 대한 설명을 빼먹을 수가 없다.

인증은 쉽게 말하면 로그인이다. 특정 서비스의 회원은 각각 다른 권한이 주어질 수 있다. 아이디와 패스워드를 통해 인증 받는 것이다.

인가는 한 번 인증을 받은 사용자가 로그인 이후에 특정 권한을 사용할 수 있는 것을 뜻한다.

서버에서는 동시 다발적으로 수많은 요청이 들어올텐데 이 요청 중 어떤 것은 로그인이 된 상태, 어떤 것은 로그인이 안 된 상태, 혹은 특수한 권한이 필요한 요청일 수도 있다. 서버는 요청에서 로그인 상태나 권한 등을 확인하고 서비스를 허용해줄지 말지를 결정해서 응답해야 한다.

🤔 요청을 할 때마다 로그인을 요구하는 방법은?
  • 사용자가 어떤 웹 사이트 상에서 링크를 클릭하는 것 하나 하나가 모두 요청이 될 수 있다. 링크를 클릭할 때마다 로그인을 요구한다면 사실상 정상적인 서비스 이용이 힘들 것이다.
  • 서버의 입장에서도 로그인을 처리하기 위해 DB에서 저장된 사용자 계정의 해시값 등을 꺼내온 후에 이것들을 사용자의 암호와 함께 복잡한 알고리즘으로 계산한 결과 값과 일치하는지 확인하는 과정을 거쳐야 하는데 이는 아주 무거운 작업이다
  • 매 요청마다 아이디와 패스워드가 실려서 날아다니면 보안상 위험하다

이러한 인증, 인가 문제를 해결하기 위해서 등장한 개념이 세션이나 JWT 등이다.

🔥세션


세션이란?

세션은 사용자가 로그인에 성공하면 세션 정보를 사용자의 브라우저와 서버의 메모리(혹은 하드나 DB)에 저장한다.

브라우저에서는 Session ID라는 이름의 쿠키로 저장하고, 이후 서버에 요청을 보낼 때 이 정보를 함께 실어 보낸다.

서버에서는 이 정보를 서버의 메모리에 있는 정보와 비교해 맞는 짝이 있으면 인가를 해주는 방식이다.

이처럼 Session ID를 사용해서 어떤 사용자가 서버에 로그인 된 상태가 지속되는 것을 세션이라고 한다.

단점

서버가 수많은 세션을 가지고 있어서 메모리가 부족해지거나 에러로 인해 서버를 재부팅하는 경우 모든 로그인 정보가 날아가게 된다.

만약 서버의 규모가 커져서 서버를 여러 대로 운영하는 경우에 요청이 들어오면 여러 서버 중 맞는 정보를 찾아 여러 서버들 사이에서 분산을 해서 로드 밸런싱을 해줘야 하는데 만약 로그인은 1번 서버에서 하고, 이메일 페이지로 가능 요청은 3번 서버에서 하면 3번 서버에는 로그인 정보가 없기 때문에 세션 유지가 제대로 안 된다.

이런 경우를 대비해 DB에 저장하는 경우 속도가 아주 느려질 수 있다.

JWT vs 세션

JWT는 위와 같은 쿠키를 사용한 세션의 단점을 커버할 수 있다.

JWT는 세션과 다르게 로그인에 성공하면 토큰을 발급하는데 이 토큰을 서버에 저장하지 않고 클라이언트에 내려준다. 이렇게 되면 서버의 부담을 덜 수 있다. (뒤에서 설명할 Refresh 토큰에서는 조금 달라질 수 있는 설명이다)

그러나 이 방식의 문제는 이미 발급된 토큰을 회수할 수 없기 때문에 통제하기 힘들다는 특징이 있다.

🔥JWT의 구조


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

위 문자들이 바로 JWT 토큰이다. 자세히 보면 . 으로 구분되어 3개의 파트로 나눌 수 있다.

jwt_encode

이 정보들은 해시 알고리즘으로 인코딩 된 정보이다. 이렇게 인코딩 된 정보를 디코딩하면 다음과 같은 정보를 얻을 수 있다.

jwt_decode

디코딩 된 정보의 구조를 보면 왜 Json Web Token 인지 알 수 있다.

.으로 구분된 3개의 영역은 각각 HEADER, PAYLOAD, VERIFY SIGNATURE이다.

PAYLOAD

우선 페이로드에 대해 살펴보겠다.

페이로드에는 실질적으로 토큰에 담겨 있는 정보인 Claim이 들어가는 부분이다. Claim은 키-값 형식으로 이루어진 한 쌍의 정보를 뜻한다. Claim의 키를 Claim Name 이라고 한다. Claim Name은 유일해야 한다.

Claim Name 3개의 종류로 구분된다. 참고

등록 클레임 이름(Registered Claim Names)

미리 정의된 7개의 클레임 이름이다.

  • iss (Issuer) Claim: 발행자
  • sub (Subject) Claim: 제목
  • aud (Audience) Claim: 수신자
  • exp (Expiration Time) Claim: 만료 시간. NumericDate 형식을 가지며 현재보다 미래의 시간으로 설정되어야 한다.
  • nbf (Not Before) Claim: 활성 날짜. nbf로 설정된 시간 이후부터 유효한 토큰이 된다. NumericDate 형식을 따른다.
  • iat (Issued At) Claim: 발행된 시간
  • jti (JWT ID) Claim: 고유한 아이디

공개 클레임 이름(Public Claim Names)

사용자가 정의할 수 있는 사용자 지정 클레임이다. 공개용 정보를 전달하기 위해 사용한다.

충돌을 방지하기 위해 URI 형식으로 짓는다.

비공개 클레임 이름(Private Claim Names)

당사자들 간에 정보를 공유하기 위해 만들어진 사용자 지정 클레임.

보통 서버와의 통신을 할 때 사용할 수 있다. 하지만 페이로드 자체가 암호화되는 것은 아니기 때문에 사용자의 아이디나 비밀번호 같은 민감한 정보를 담지 않는다.

공개 클레임 이름과 달리 충돌이 될 수도 있다.</br>

payload

위 사진에서 sub, iat은 등록 클레임 이름, name은 비공개 클레임 이름이 된다.

header

헤더에는 typalg를 가진다.

typ: JWT는 이 속성이 항상 JWT로 고정이다. 정확히 말하면 이 부분이 JWT로 되어 있어야 JWT로서 동작할 수 있다. alg: 해싱 알고리즘. 뒤에 설명할 Signature 값을 만드는데 사용될 알고리즘이다. 어떠한 해싱 알고리즘이 사용되었는지 명시되는 부분이다. 주로 HMAC SHA256이나 RSA가 사용된다. 위 사진에서는 HMAC SHA256이 사용된 모습이다.

VERIFY SIGNATURE

signature

서명은 헤더의 인코딩 값과 페이로드의 인코딩 값을 합쳐서 (서버에 저장된) 비밀키로 해시를 생성한다. (이때 해시를 만드는 해시 알고리즘은 헤더에서 지정한 알고리즘을 사용한다)

이렇게 만든 해시 값을 base64로 인코딩하면 클라이언트가 받아볼 수 있는 형태가 된다.

아래 홈페이지에서 확인해 볼 수 있다.

pic_homepage2

🔥JWT 안전한가?


JWT는 HMAC나 RSA, ECDSA 등으로 디지털 서명되기 때문에 JWT를 암호화하는 키의 안정성이 즉 JWT의 안정성이라고 할 수 있을 것 같다.

JWT를 암호화 하는 과정은 서버에서 이루어지기 때문에 서버에서 비밀키를 잘 관리해야 한다.

그러나 세션과 마찬가지로 JWT 토큰 자체를 탈취당하면 쉽게 도용당할 수 있다.

🔥Access 토큰과 Refresh 토큰


JWT 토큰 자체를 탈취당하면 쉽게 도용당할 수 있다는 단점 때문에 Access 토큰Refresh 토큰의 개념이 등장했다.

만약 JWT 토큰이 탈취 당하더라도 토큰의 유효 기간이 남아 있는 동안에만 도용할 수 있으므로 토큰의 유효 기간을 짧게 설정한다면 위협의 기회를 낮출 수 있다.

하지만 토큰의 유효 기간이 짧다는 것은 정상적인 사용자도 불편함을 겪게 될 것이라는 이야기가 된다.

예를 들어 만료 기간이 3분이라고 하면 로그인 후 3분이 지나면 다시 로그인을 해야 하는 등의 불편함이 생긴다.

이를 해결하기 위한 방안이 Refresh 토큰이다. Refresh 토큰Access 토큰을 재발급 하기 위한 토큰이다.

  1. 사용자가 로그인을 한다
  2. 인증에 성공하면 서버는 두 개의 토큰을 내려준다
    1. Access 토큰: 수명이 몇 시간/분 이하로 짧다
    2. Refresh 토큰: 수명이 2주 정도로 길게 잡혀있다
  3. 2번에서 서버가 두 개의 토큰을 내려줄 때 Refresh 토큰에 상응하는 값을 DB에 저장한다
  4. 사용자가 Access 토큰을 사용할 때 만료 기간이 종료되었다면 Refresh 토큰으로 새 Access 토큰을 요청한다
  5. 서버는 DB에 저장된 값과 비교해 새로운 Access 토큰을 내려준다

Refresh 토큰만 안전하게 관리된다면 Refresh 토큰이 유효한 동안에는 Access 토큰이 만료될 때마다 다시 로그인을 할 필요 없이 재발급을 받을 수 있다는 것이다.

카카오의 토큰 만료 시간 pic_kakao_token

위 사진은 카카오 로그인 API 문서의 일부이다. (사진을 클릭하면 이동해서 확인할 수 있다)

해당 문서에서 카카오 로그인의 경우도 JWT를 사용한다고 명시되어 있다.

사진에서 볼 수 있는 것과 같이 Refresh 토큰의 만료 시간이 어느 정도 이하로 남았다면 갱신하면서 좀 더 사용성을 높일 수 있는 방법을 채택하기도 한다.

기존 방식과 차이점

  • 앞서 설명했던 JWT 특징에서 서버에 저장하지 않는다고 설명했는데 Refresh 토큰을 사용하게 되면 Refresh 토큰(혹은 상응 값)을 서버에서 가지고 있어야 한다.
  • 토큰을 한 번 발급하면 토큰의 만료 기간 내에는 제어할 수 없지만 Access 토큰의 만료 기간이 짧아지면서 서버의 DB에서 상응하는 Refresh 토큰을 제거하면 Access 토큰이 만료된 후에는 재 로그인이 필요하다.

추가로 고려할 점

  • Refresh 토큰의 안전한 저장
  • SSL/TLS1.3 (HTTPS)를 통한 토큰 전달

🔥마무리


SUWIKI 프로젝트에서 자체 로그인을 구현하면서 JWT를 도입했다. 사실 트래픽이 많이 발생하는 서비스를 개발하는 것이 아니기 때문에 서버의 부담을 줄이기 위해서 JWT를 택한 것은 아니고 안드로이드와 iOS도 같이 개발하고 있어서 쿠키를 사용한 세션 방식은 까다로울 것으로 생각해서 JWT 방식이 가장 깔끔할 것으로 생각되어서 선택했던 걸로 기억한다.

사실 토큰을 발급하는 것은 서버에서 담당했고, 내가 맡은 안드로이드에서는 로그인 후 서버에게 받은 토큰을 잘 저장하고 API 호출 시에 잘 보내주기만 하면 되는 것이라 사용하는데 큰 어려움은 없었지만 이렇게 글을 작성해보면서 다시 한 번 공부하게 되면서 JWT에 대해서 정확히 모르고 사용하고 있었다는 것을 깨달았다.

참고