DRF에서 JWT(JSON Web Token) 사용하는 방법

DRF에서 JWT(JSON Web Token) 사용하는 방법

2018, Nov 25    
  • DRF(Django Rest Framework)에서 기본 적으로 지원하는 Token은 단순한 랜덤 문자열 입니다.
    • 각 User와 1:1 매칭을 이용합니다.
    • 유효 기간이 없습니다.
import binascii
import os

binascii.hexlify(b'12345').decode()

>> '3132333435'


즉, DRF 기본 Token은 랜덤 문자열 이므로 의미 있는 데이터를 가지고 있지 않습니다.

반면에, JWT는 토큰 자체가 데이터를 가지고 있습니다.

JWT는

  • 또 다른 형태의 Token 입니다.
  • 데이터베이스를 조회하지 않아도 로직만으로도 인증이 가능합니다.
  • 토큰 포맷 : 헤더.내용.서명
  • 서버 측에서 토큰 발급 시에 비밀키로 서명을 하고, 발급시간 claim으로서 exp를 사용합니다.
    • claim은 Key/value 형식의 정보입니다.
    • 토큰을 요청한 유저 측에서는 유저네임패스워드를 제공을 해야하고, 유저네임과 패스워드가 맞으면 Token을 발급해 주는데,
      이 때, 서버에서 임의로 정한 비밀키로 서명을 하고 발급시간exp 라는 필드에 담아서 발급을 해줍니다.
  • 서명을 하는 것일 뿐, 암호화는 아닙니다. 누구라도 열어볼 수 있으므로 보안에 중요한 데이터를 넣으면 안됩니다. 필요한 최소한의 정보만 담는 것이 권장됩니다.
    • 인증 요청 시 마다 토큰을 보내야 하므로 필요 없는 정보가 포함되면 매 번 비용이 많아지므로 최소한의 정보만 담는게 좋습니다.
  • djangorestframework-jwt에서는 Payload 항목에 디폴트로 user_id, user_name, e-mail 이름의 claim을 사용합니다.
    • id / name / mail 정보가 Token 자체에 있으므로 DB를 조회하지 않아도 누구인지 인증이 가능합니다.
    • Payload = id + name + e-mail + exp 이고 토큰 포맷의 헤더.내용.서명 중에 내용에 해당합니다.
  • 갱신(Refresh) 매커니즘을 지원합니다.
    • Token 유효기간 내에 갱신하거나, 유저네임/패스워드를 통해 재 인증 받아야 합니다.

- Token은 반드시 안전한 장소에 보관해야 합니다.

  • 일반 Token / JWT 토큰 여부에 상관 없이 Token은 반드시 안전하게 보관 되어야 합니다. (인증 관련 요소이므로)
  • 스마트폰 앱은, 설치된 앱 별로 안전한 저장 공간이 제공되므로 토큰 방식을 쓰는 것이 좋지만 웹브라우저를 사용할 때는 저장 공간이 없습니다.
    • Token은 앱 환경에서만 권장 됩니다.
    • 웹 클라이언트 환경에서는 보안적인 측면에서 세션 인증이 나은 선택일 수도 있습니다.

- Token 예시

8df73dafbde4c669dc37a9ea7620434515b2cc43

- JWT 예시

ex) J0eXAi…IUzI1NiJ9.eyJ1c2VyX2lkIjo…aWwiOiIifQ.Zf_o3S7Q7-cmUz…LcF-2VdokJQ (너무 길어서 … 로 줄였습니다.)

  • JWT는 . 을 기준으로 3영역으로 나뉘게 됩니다.
  • 헤더(Header)를 base64 인코딩하여, eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
  • 내용(Payload)를 base64 인코딩하여, eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFza2RqYW5nbyIsImV4cCI6MTUxNTcyMTIxMSwiZW1haWwiOiIifQ
  • 서명(Signature) : Header/Payload를 조합하고 비밀키로 서명한 후 base64 인코딩하여, Zf_o3S7Q7-cmUzLWlGEQE5s6XoMguf8SLcF-2VdokJQ

- DRF에 JWT 세팅하기

  • 먼저 패키지를 설치 합니다.
    • pip install djangorestframework-jwt
  • 프로젝트/urls.py 에서 패키지 및 url을 등록해줍니다.
    • obtain_jwt_token : JWT 토큰 획득
    • refresh_jwt_token : JWT 토큰 갱신
    • verify_jwt_token : JWT 토큰 확인
project/urls.py

from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token, verify_jwt_token

urlpatterns = [
    ... ,
    url(r'^api-jwt-auth/$', obtain_jwt_token),          # JWT 토큰 획득
    url(r'^api-jwt-auth/refresh/$', refresh_jwt_token), # JWT 토큰 갱신
    url(r'^api-jwt-auth/verify/$', verify_jwt_token),   # JWT 토큰 확인
]


  • 프로젝트/settings.py 에서 REST_FRAMEWORK 의 세팅값을 수정해 줍니다.
REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES' : [
        ...
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ]
}


  • 프로젝트/settings.py 에서 JWT_AUTH 에서 JWT 갱신 허용을 True로 설정해 줍니다.
JWT_AUTH = { 
    'JWT_ALLOW_REFRESH': True, 
}


- HTTPie를 통한 JWT 발급

다양한 언어 또는 방법을 통해서 특정 URL에 POST를 하는 방법이 있습니다. 그 중, HTTPie를 이용하여 POST를 해보겠습니다.

http POST http://localhost:8000/api-jwt-auth/ username="유저명" password="암호"
{
    "token": "인증에 성공할 경우, 토큰응답이 옵니다."
}    


  • 이 때 사용할 유저명과 암호는 예를 들어서 createsuperuser로 만들었던 유저명과 암호를 사용하시면 실습해 볼 수 있습니다.
  • 인증에 실패할 경우 400 Bad Request 응답을 받습니다.
  • 발급 받은 JWT Token을 https://jwt.io/ 에 입력하면 어떤 의미를 가지고 있는지 알 수 있습니다.
    • jwt.io에서 Signature를 인증할 때 Seceret Code는 프로젝트/settings.py에 있는 SECRET_KEY를 입력하면 됩니다.
  • 정상적으로 POST가 되면 다음과 같은 Token이 발급 됩니다.
    • ex) eyJ0eXAiO3JKV2QeL…
    • 발급 받은 Token의 형태를 보면 앞에서 언급한 바와 같이 .을 기준으로 3부분으로 구분됩니다.

- 발급받은 JWT Token 확인

verify 를 통하여 JWT Token으로 인증이 잘 되는지 확인할 수 있습니다.

http POST http://localhost:8000/api-jwt-auth/verify/ token="eyJ0eXAiO3JKV2QeLCJhdGci..."


  • 이 때 인증이 잘 되면 입력한 Token이 그대로 반환 됩니다. ex) eyJ0eXAiO3JKV2…
  • 만약 exp가 만료가 되었다면 다음과 같이 반환 됩니다.
{
    "detail": "Signature has expired."
}


- 발급받은 JWT Token으로 포스팅 목록 API 요청

Authorization이 필요한 Application을 접근할 때 JWT를 이용하여 ListView를 하려면 아래와 같이 할 수 있습니다. 아래에서 app/post/는 예시 입니다. “Authorization: JWT 토큰”에서 토큰 부분에 실제 토큰을 입력하면 됩니다. ex) “Authorization: JWT eyJ0eXAiO3JKV2…”

http http://localhost:8000/app/post/ "Authorization: JWT 토큰"


정리를 하면,

  • DRF Token에서는 인증헤더 시작문자열로 “Token”을 사용하였지만, JWT 에서는 “JWT”를 사용합니다.
  • 인증이 성공할 경우 해당 API의 응답을 받습니다.
  • 이제 매 API 요청마다, 필히 JWT 토큰을 인증헤더에 담아 전송해야 합니다.

- JWT Token 유효기간이 지났을 경우

앞에서 설명드린 바와 같이 JWT Token의 유효기간이 지났을 경우에는 다음과 같이 반환받습니다.

http http://localhost:8000/app/post/ "Authorization: JWT 토큰"

HTTP/1.0 401 Unauthorized
{ 
    "detail": "Signature has expired."
}


  • 클라이언트(앱) 에서는 토큰 유효기간 내에 토큰을 갱신 받아야 합니다. 유효 기간이 지났을 경우, 위와 같이 “401 Unauthorized” 응답을 받게 됩니다.
  • 유효 기간 내에는 Token 만으로 갱신을 받을수 있습니다.
  • 유효 기간이 지나면 username/password를 통해 인증 받아야 합니다.
  • JWT 토큰 유효기간은 settings.JWT_AUTHJWT_EXPIRATION_DELTA 값을 참조 하며 기본 값은 5분 입니다.

- JWT Token 갱신받기

반드시 Token 유효 기간 내에 갱신이 이루어 져야 합니다.

http POST http://localhost:8000/api-jwt-auth/refresh/ token="토큰"
{ 
    "token": "갱신받은 JWT 토큰" 
}



  • settings.JWT_AUTH의 JWT_ALLOW_REFRESH 설정은 디폴트가 False 입니다. True 설정을 해야 갱신을 진행할 수 있습니다.

- djangorestframework-jwt의 주요 settings

JWT_AUTH = { 
    'JWT_SECRET_KEY': settings.SECRET_KEY, 
    'JWT_ALGORITHM': 'HS256', 
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300), 
    'JWT_ALLOW_REFRESH': False, 
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), 
}


기본적인 Setting 값은 위와 같습니다. 여기서 바꿔줘야 하는 것은 JWT_EXPIRATION_DELTA, JWT_ALLOW_REFRESH, JWT_REFRESH_EXPIRATION_DELTA 항목 입니다.

  • JWT_EXPIRATION_DELTA : Token 만료시간으로 기본값은 5분 입니다. 5분은 너무 짧으니 늘려 줍시다.
  • JWT_ALLOW_REFRESH : Token Refresh가 가능하도록 해줘야 합니다. 따라서 True로 바꾸어 줍니다.
  • JWT_REFRESH_EXPIRATION_DELTA : Refresh 가능 시간 입니다. 상식적으로 만료 되기전에 Refresh를 하는게 맞으므로 위의 JWT_EXPIRATION_DELTA 시간보다 짧게 해주면 됩니다.



실제 SNS랑 연동해서 사용해 보는 방법에 대하여 본격적으로 확인해 봅시다.

카카오를 예를 들어 설명해 보겠습니다. 카카오로부터 Access Token을 획득하고, 이를 장고 서버를 통해 JWT 토큰을 획득해야 합니다.

  • 앱 ~ 카카오톡 서버
    • 안드로이드 앱에서 “카카오톡 로그인” 버튼을 클릭하면, 카카오톡 서버와 인증을 수행합니다.
    • 카카오톡 서버와의 인증에 성공하면, 카카오톡으로부터 Access Token 을 획득 합니다.
      • Access Token은 안드로이드/ios에서 Native API를 통해 얻습니다. 이 또한 만료 시간은 있지만 만료 시간이 깁니다.
  • 앱 ~ 장고 서버
    • 획득된 Access Token을 장고 서버 인증 Endpoint (/accounts/rest-auth/kakao/)를 통해, JWT 토큰 획득
      • /accounts/rest-auth/kakao/ 는 django 서버에서 따로 설정해 주어야 하는 URL 입니다.
      • kakao 말고 따른 SNS 를 추가 하고 싶으면 /accounts/rest-auth/facebook/ 과 같이 설정할 수 있도록 유연하게 설계하면 됩니다.
    • 획득한 JWT 토큰이 만료되기 전에, 갱신 합니다.
    • 획득한 JWT 토큰이 만료되었다면, Access Token을 서버로 전송하여 JWT 토큰 재획득

1) 카카오 개발자 홈페이지에서 필요한 정보를 가져옵니다.

https://developers.kakao.com 를 접속하여 로그인한 후에 Application을 하나 만들어 보겠습니다.

1

여기서 네이티브 앱 키는 스마트폰 앱에서 사용할 것이고 REST API 키는 장고 서버에서 사용할 것입니다. 네이티브 앱 키는 정확히 말하면 안드로이드/아이폰 앱 등에서 kakao_app_key 값으로 활용됩니다.

2

사용자 관리탭에서 사용자 관리 사용 옵션을 ON으로 켜주고, 수집 목적등을 기입한 후 저장합니다.

2) 장고 서버에 SNS(카카오) 등록 및 REST API 설정을 합니다.

장고 서버의 설정은 https://django-rest-auth.readthedocs.io/en/latest/installation.html 을 따라하면 됩니다.

2-1) rest-auth 설치

  • 먼저 패키지를 설정 합니다.
pip install django-rest-auth


  • rest_auth를 project/settings.py의 INSTALLED_APP에 추가 합니다.
INSTALLED_APPS = (
    ...,
    'rest_framework',
    'rest_framework.authtoken',
    ...,
    'rest_auth'
)


참고로 django-rest-auth를 수행하기 위해서는 당연히 django-rest-framework 환경 설치가 선 진행 되어야 합니다. 따라서 다음을 먼저 설치해 주었는지 확인합니다.

pip install djangorestframework


  • rest_auth를 project/urls.py에 등록 합니다.
urlpatterns = [
    ...,
    url(r'^rest-auth/', include('rest_auth.urls'))
]


  • 만약 새로 만든 프로젝트라면 migrate 를 한번 해줍니다.
python manage.py migrate


2-2) django-allauth 설치 및 Registration

  • 이제 django-allauth 라는 것을 설치해 주어야 합니다. 설치할게 많은데요… 이것은 SNS 계정과 연동하기 위한 패키지 입니다.
    아래 명령어를 통해 설치하시면 됩니다.
pip install "django-rest-auth[with_social]"


  • 패키지를 설치를 하였으니 project/settings.py 의 INSTALLED_APPS 에 아래 내용을 등록해 줍니다.
INSTALLED_APPS = (
    ...,
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'rest_auth.registration',
)


django.contrib.sitesMulti sites 기능을 지원해 주는 것을 뜻합니다. 간단하게 설명하면 Multi sites는 하나의 사이트에서 여러 domain을 가질 수 있는 기능을 말합니다. Model을 설정할 때, site의 ID를 ForeignKey로 설정할 수 있습니다. 예를 들면 다음과 같습니다.

class Post(models.Model):
    site = models.ForeignKey(SITE_ID)
    ...


이렇게 ForeignKey를 설정해 주면 하나의 모델이지만 여러 서비스에서 사용될 수 있습니다. 기본적으로 django-allauth 는 multi sites 기능을 통하여 구현되기 때문에 INSTALLED_APPS에 django.contrib.sites를 입력해 주어야 합니다. 하지만 저희는 여러 사이트를 운영하는 것은 아니고 한 개의 사이트만 운영할 것이기 때문에 Site 아이디를 강제로 지정해주어야 합니다.

따라서 project/settings.py에 다음과 같이 SITE_ID를 강제로 지정합니다.

SITE_ID = 1


  • project/urls.py 에서 rest-auth/registration/을 등록합니다.
urlpatterns = [
    ...,
    url(r'^rest-auth/', include('rest_auth.urls')),
    url(r'^rest-auth/registration/', include('rest_auth.registration.urls'))
]


2-3) Social Authentication 등록

이제 INSTALLED_APPS에 allauth.socialaccount, allauth.socialaccount.providers.facebook 을 추가합니다. 앞의 과정을 계속 따라오셨다면 INSTALLED_APPS는 다음과 같습니다.

INSTALLED_APPS = [
    ...,
    'rest_framework',
    'rest_framework.authtoken',
    'rest_auth'
    ...,
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'rest_auth.registration',
    ...,
    'allauth.socialaccount',
    'allauth.socialaccount.providers.kakao',
]


만약 kakao가 아니라 facebook을 등록하고 싶으시면 allauth.socialaccount.providers.kakao 에서 kakao만 facebook으로 바꾸면 됩니다.

2-4) SNS 등록

SNS 계정을 등록할 app을 하나 만들어서 url을 관리해 보도록 하겠습니다.

  • accounts 라는 앱을 만들고 project/urls.py에 등록 합니다.
# project/urls.py

urlpatterns = [
    ...
    url(r'^accounts/', include('accounts.urls')),
    ...
]


  • accounts/urls.py를 만들고 아래와 같이 입력 합니다.
# accounts/urls.py

from django.conf.urls import include, url
from .views import KakaoLogin

app_name = 'accounts'

urlpatterns = [
    url(r'^rest-auth/kakao/$', KakaoLogin.as_view(), name='kakao_login'),
]


  • accounts/views.py를 만들고 아래와 같이 입력 합니다.

from allauth.socialaccount.providers.kakao.views import KakaoOAuth2Adapter
from rest_auth.registration.views import SocialLoginView

class KakaoLogin(SocialLoginView):
    adapter_class = KakaoOAuth2Adapter
    


2-5) JWT 세팅

django-rest-auth는 기본적으로 Django의 Token 기반 인증을 수행합니다. 여기서 JWT 인증을 적용할 것이기 때문에 아래 내용을 실행해야 합니다.

pip install djangorestframework-jwt


  • rest-framework에서 사용할 인증 방식을 JWT로 정하기 위하여 project/settings.py에서 아래와 같이 옵션을 지정해 줍니다.
REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': [
         ...         
         'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ],    

}


  • 이제 rest-framework에서 Token 발급 시 JWT를 기본으로 사용하도록 명시하겠습니다. project/settings.py에 아래 내용을 추가합니다.
REST_USE_JWT = True


2-6) django admin 에서 세팅

자 이제 다했습니다. 그럼 구현한 내용을 보기 위해서 admin 사이트 (localhost:8000/admin)에 들어가보겠습니다.

admin1


admin 접속 후 SITES 항목에 들어가면 example.com이 한 개 생성되어 있습니다. 여기를 들어가 보겠습니다.

admin2


URL을 보면 http://localhost:8000/admin/sites/site/1/change/ 에서 1을 볼 수가 있습니다. 여기서 숫자 1이 저희가 지정한 SITE_ID = 1에 해당합니다.

이제 해야할 마지막 작업은 SNS를 admin에 등록해야 합니다. 처음에 만든 카카오 개발자 홈페이지에서 네이티브 앱 키, REST API 키 등이 있었습니다. 이 중 REST API 키를 Django 서버에서 사용한다고 하였는데 이 것을 등록해야 합니다.

  • Django admin의 Social Accounts › Social applications › Add social application 를 클릭합니다.

admin3


위와 같이 셋팅을 해주고 등록을 하면 드디어 카카오가 등록 되었습니다.

앱에서 로그인 하기 전에 실제로 서버가 잘 동작하는지 확인하기 위해 access_token을 이용해 보겠습니다.

kakao5


카카오 개발자 페이지에서 개발가이드 >> REST API 도구에 들어가서 앱을 선택하고 로그인 하면 Access Token이 생성 됩니다.

3) 앱을 수정합니다.

  • baseURL 주소를 장고 서버에 맞게 설정 합니다.
  • android-app/app/build.gradle 경로의 applicationId에 유일한 아이디를 할당합니다.
    • 경로 내의 android { defaultConfig { applicationId … } }
    • applicationId는 앱 별로 유니크 하며 playstore에 한번 올리면 변경할 수 없습니다.
  • android-app/app/src/main/res/values/strings.xml 에서 app_namekakao_app_key를 변경합니다.
    • app_name에는 사용자가 지정하고 싶은 앱 이름을 넣으면 됩니다.
    • kakao_app_key 에는 네이티브 앱 키를 지정하면 됩니다.
<resources>
    <string name = "app_name"> App with kakao </string>
    <string name = "kakao_app_key"> 99ca10c5dd3b1da637b37519b2cc9493 </string>   
</resources>


다시 한번 참조하면 네이티브 앱 키는 아래에서 확인할 수 있습니다.

1


  • 안드로이드 앱 빌드 & 실행을 통해, keyHash 값 확인/복사 합니다. 디버깅 창에서 확인할 수 있습니다.

3

패키지명에는 위에서 설정한 applicationId를 넣습니다. 마켓 URL은 자동 설정 됩니다.

4

안드로이드 스튜디오에서 확인한 keyHash 값을 사이트에 등록하면 됩니다.