Django REST Framework JWT를 알아보자
Web/Django

Django REST Framework JWT를 알아보자

뉴비뉴 2020. 3. 21.

회사 코드를 읽던 중 계정 관련 레포를 보다가 JWT와 관련 된 내용이 있어서

정확히 어떻게 동작하는지 공식문서와 블로그를 읽은 내용을 바탕으로 정리하였습니다.

 

먼저 JWT가 뭔지 알아볼 필요가 있습니다.

JWT란 Json Web Token 입니다. 이름만봐도 웹 토큰의 역할을 하고 있다는 것을 확인할 수 있습니다.

그럼 토큰이란 무엇일까, 토큰을 갖고 있다는 것을 어떠한 행동을 할 수 있는 권한을 갖고 있다는 것과 같습니다.

 

정리해보면 우리는 로그인하기 위해서 이 사용자가 인증받은 사용자인지 확인할 필요가 있고,

사용자가 로그인 시 토큰을 발행해 웹 사이트 안에서 본인이 가진 토큰(권한)으로 글도 작성하고,

여러 작업들을 할 수 있을 겁니다.

 

토큰 기반 인증 시스템을 선택하는 이유를 잘 정리해둔 블로그가 있어 링크를 첨부합니다.

(일부내용을 발췌하였습니다. 자세한 내용은 위 링크에서 확인해주세요.)

 

1. Stateless 서버 구성

Stateless의 경우 클라이언트와 서버의 연결고리가 없기 때문에 서버의 확장성이 높아집니다.

Stateful의 경우 유저가 인증을 하게 되면 서버는 세션정보를 저장해야 됩니다.

대부분의 경우엔 메모리에 저장하는데, 유저 수가 증가한다면 서버의 메모리가 많은 정보가 쌓이게되고,

이를 피하기 위해서 세션을 데이터베이스에 저장을 하게 되지만 이 또한 JWT에 비교하면 그렇게 효율적인 방법은 아닙니다.

세션을 사용하게 되면 분산된 시스템을 설계하는 것이 불가능한것은 아니지만 과정이 매우 복잡해집니다.

 

2. 모바일 어플리케이션에 적합하다.

안전한 API를 만들기 위해선 쿠키같은 인증시스템은 이상적이지 않습니다.

 

3. 인증정보를 다른 어플리케이션으로 전달

대표적인 예로는 OAuth가 있습니다. 소셜 로그인 께정들을 이용하여 다른 웹서비스에서도 로그인 할 수 있습니다.

 

4. 보안

보안을 높일 수 있습니다.

 

작동 원리

1. 유저가 아이디와 비밀번호로 로그인을 합니다

2. 서버측에서 해당 계정정보로 검증합니다.

3. 계정정보가 정확하다면, 서버측에서 유저에게 signed 토큰을 발급해줍니다.

4. 클라이언트 측에서 잔달받은 토큰을 저장해두고, 서버에 요청을 할 때 마다, 해당 토큰을 함께 서버에 전달합니다.

5. 서버는 토큰을 검증하고, 요청에 응답합니다.

 

웹서버에서 토큰을 서버에 전달할 때에는 HTTP 요청의 헤더에 토큰값을 포함시켜서 전달합니다.

 

여기서 궁금한 점들이 생기기 시작했습니다.

토큰을 어디에 저장시킬까? 구글링해보니 크게 두 가지의 방법이 나왔습니다.

1. Web Storage

  • Local Storage
    • 삭제하지 않는 이상 데이터가 보존된다.
    • 브라우저를 종료해도 데이터가 보존된다.
  • Session Storage
    • 현재 페이지와 방문할 페이지에서만 사용 가능하다.
    • 브라우저를 종료하면 데이터는 삭제된다.
  • 단점
    • Local Storage는 쿠키와 다르게 특정도메인으로 부터 격리되어 있어 데이터는 서브도메인과 같이 다른 도메인에서 접근이 불가능합니다.
    • Web Storage는 같은 도메인의 자바스크립트로 접근가능하기 때문에 XSS 공격으로부터 취약합니다.

2. Cookies

  • 쿠키는 브라우저 종류 후 삭제될 수 있다.
  • 서버단에서 체크하여 만기를 설정할 수 있다.
  • 또한 쿠키는 자바스크립트 뿐만 아니라 서버단의 코드로 읽을 수 있고 HTTPonly 옵션 사용 시 서버단에서만 읽을 수 있다.
  • 단점
    • 쿠키의 최대 저장용량은 4KB 이므로 토큰에 많은 데이터를 저장하고자 할 때 문제가 생길 수 있다.
    • 쿠키는 CSRF나 XSRF 공격으로 부터 취약하다.
curl http://127.0.0.1:8000/hello/ -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzODI4NDMxLCJqdGkiOiI3ZjU5OTdiNzE1MGQ0NjU3OWRjMmI0OTE2NzA5N2U3YiIsInVzZXJfaWQiOjF9.Ju70kdcaHKn1Qaz8H42zrOYk0Jx9kIckTn9Xx7vhikY'

Authorization: Bearer가 뭘까라는 궁금즘에 구글링을 해보았습니다.

https://security.stackexchange.com/questions/108662/why-is-bearer-required-before-the-token-in-authorization-header-in-a-http-re

 

Why is 'Bearer' required before the token in 'Authorization' header in a HTTP request?

What exactly is the difference between following two headers: Authorization : Bearer cn389ncoiwuencr vs Authorization : cn389ncoiwuencr All the sources which I have gone through, set...

security.stackexchange.com

 

The JWT is acquired by exchanging an username + password for an access token and an refresh token.

 

The access token is usually short-lived (expires in 5 min or so, can be customized though).

 

The refresh token lives a little bit longer (expires in 24 hours, also customizable). It is comparable to an authentication session. After it expires, you need a full login with username + password again.

 

username과 password를 입력하여 로그인하면 access token과 refresh token을 획득할 수 있다.

access token은 5분이 지나면 만료되고, refresh token은 24시간이 지나면 만료되고, 다시 username과 password를 입력해야 합니다. 이 시간은 커스터마이징 할 수 있습니다.

 

It’s a security feature and also it’s because the JWT holds a little bit more information. If you look closely the example I gave above, you will see the token is composed by three parts:

위 코드블럭을 보면 3개의 파트로 나뉘어져 있는 걸 확인할 수 있다.

header.payload.signature

header = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload = eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzODI4NDMxLCJqdGkiOiI3ZjU5OTdiNzE1MGQ0NjU3OWRjMmI0OTE2NzA5N2U3YiIsInVzZXJfaWQiOjF9
signature = Ju70kdcaHKn1Qaz8H42zrOYk0Jx9kIckTn9Xx7vhikY

위 정보는 BASE64 로 encode하고, decode하면 아래와 같은 결과가 나옵니다.

header

JWT인 토큰의 유형이나 HMAC SHA256 또는 RSA와 같이 사용되는 Hash 알고리즘이 무엇으로 사용했는지 등 정보가 담긴다.

{
  "typ": "JWT",
  "alg": "HS256"
}

payload

서버에서 보낼 데이터가 들어갑니다.

일반적으로 사용자의 ID, 유효기간(exp) 포함

{
  "token_type": "access",
  "exp": 1543828431,
  "jti": "7f5997b7150d46579dc2b49167097e7b",
  "user_id": 1
}

signature

Base64방식으로 인코딩한 Header, payload 그리고 SECRET KEY를 더한 후 서명된다.

 

The signature is issued by the JWT backend, using the header base64 + payload base64 + SECRET_KEY. Upon each request this signature is verified. If any information in the header or in the payload was changed by the client it will invalidate the signature. The only way of checking and validating the signature is by using your application’s SECRET_KEY. Among other things, that’s why you should always keep your SECRET_KEY secret!

 

Base64방식으로 인코딩 했다는 것은 다시 디코딩도 가능하다는 것을 뜻 합니다.

따라서 payload에는 중요한 정보가 포함되면 안됩니다.

 

그렇다면 인증받은 A가 B의 데이터를 보고자 payload를 조작하여 B의 아이디로 인코딩 후 서버에 해당 토큰을 전달합니다. 그러면 서버는 처음 암호화 된 (A의 토큰) signature를 검사하게 됩니다. 여기서 payload는 B 사용자의 정보가 들어있으나, signature는 A의 payload를 기반으로 암호화되어 있기 때문에 유효하지 않는 토큰으로 간주합니다.

따라서 A의 사용자는 B의 SECRET KEY를 알지 못하는 이상 토큰을 조작할 수 없습니다.

 

Access Token, Refresh Token

JWT를 통한 인증 방식의 문제로 토큰을 탈취 당할 경우 보안에 취약하다는 점 입니다.

유효기간이 짧은 Token의 경우, 사용자는 새 Token을 발급받기 위해 로그인을 자주 시도해야 하고 유효기간을 늘리면 탈취 당했을 때 보안에 취약해지게 됩니다. '유효기간을 짧게 하면서 보안을 챙길 수 있는' 물음에 나온 방식이 Refresh Token 입니다.

Refresh Token은 긴 유효기간을 갖고 발행되고, Access Token의 유효기간이 만료되었을 때 새로 Token을 발급해주는 열쇠가 됩니다. 예를 들어, Refresh Token의 유효기간은 2주, Access Token의 유효기간은 1시간으로 설정하게 되면 사용자에게 발급된 Token이 1시간이 지나게 되면 Access Token은 만료되지만 Refresh Token의 유효기간은 아직 남아있기 때문에 사용자의 재로그인 없이 Access Token을 새롭게 발급받을 수 있습니다.

 

하지만 그렇다고 Refresh Token을 길게 주게 되면 어떤 문제가 발생할까?

온라인 커뮤니티를 운영하는데 불량 유저를 로그인하지 못하게끔 설정하고 싶다. 하지만 Token은 유효기간을 설정한 채로 사용자에게 주어졌기 때문에 그 기간동안 로그인을 할 수 있을 것이다. 물론 다른 방법으로 막을 순 있겠지만 추가 기능이나 작업이 추가된다.

 

일반적으로 회원 DB에 Refresh Token을 저장한다.

 

이론은 이정도까지 알아보고, 실습을 해보겠습니다

 

https://simpleisbetterthancomplex.com/tutorial/2018/12/19/how-to-use-jwt-authentication-with-django-rest-framework.html

 

How to Use JWT Authentication with Django REST Framework

JWT stand for JSON Web Token and it is an authentication strategy used by client/server applications where theclient is a Web application using JavaScript an...

simpleisbetterthancomplex.com

api/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('hello/', views.HelloView.as_view(), name='hello'),
]

simplejwt/urls.py

 

from django.contrib import admin
from django.urls import path, include

from rest_framework_simplejwt import views as jwt_views

urlpatterns = [
    path('admin/', admin.site.urls),

    path('api/token/', jwt_views.TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),

    path('', include('api.urls'))
]

 

Access, Refresh Token 생성

/api/token/ 으로 post 요청 시 username과 password를 입력하면

refresh token과 access token을 return 해주는 것을 확인할 수 있습니다.

 

여기서 나온 access token을 복사해서 인증 된 사용자만 접속가능한 /hello/ 로 접속을 시도해보겠습니다.

 

발급 된 토큰으로 인증 된 사용자만 접근가능한 hello 접근

정상적으로 Hello, World가 출력되는 것을 확인할 수 있습니다.

추가로 access token은 발급되고나서 5분의 유효기간을 갖고 있습니다.

5분 뒤에 시도하면 어떤 결과가 나올지 한번 입력해보겠습니다.

 

Access Token 발급 후 5분 경과 접근 재시도

 

5분이 지나고 요청을 하니 401 Code와 함께 유효하지 않은 토큰이라고 접속을 제한합니다.

 

Refresh Token으로 Access Token 요청

유효된 Access Token을 갱신하려면 갖고 있던 Refresh Token으로 요청하여 새로운 Access Token을 받을 수 있습니다.

지금은 튜토리얼 환경이지만 실제 프로젝트라면 Refresh Token은 DB 안에 사용자 필드에 저장을 시켜야 할 것 입니다.

 

정상적으로 Access Token이 발급되는 것을 확인할 수 있습니다.

Refresh Token 에도 토큰발급 유효기간이 있습니다.

 

튜토리얼은 간단해보이지만 실제로 쿠키나 세션에 저장하고, 토큰을 주고받고, DB에 Refresh Token을 저장하는 등

해야할게 많아보입니다. 다음번엔 응용하여 실제 쿠키나 세션에 한번 저장해보겠습니다.

댓글

💲 추천 글