DRF 에서 지원하는 기본 Token 은 단순한 랜덤 문자열 입니다.
""" Token Example """
>> import binascii
>> import os
>> binascii.hexlify(os.urandom(20)).decode()
ff199ba3d83de440a31e55c48494f906c8cfae2
쉘에서 Token 을 출력해보면 랜덤 문자열임을 확인해 볼 수 있습니다. 그러나 이를 이용해 의미 있는 데이터를 다루기는 어려워 보입니다.
반면 JWT 는 토큰 자체가 데이터를 가지고 있어, 데이터베이스를 조회하지 않아도 로직만으로 인증이 가능합니다.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFza2RqYW5nbyIsImV4cCI6MTUxNTcyMTIxMSwiZW1haWwiOiIifQ.Zf_o3S7Q7-cmUzLWlGEQE5s6XoMguf8SLcF-2VdokJ
Header를 base64 인코딩
alg
: 해싱 알고리즘
typ
: 토큰 유형
Payload를 base64 인코딩
sub/name
: 토큰 제목
iat
: 토큰 발급 시간. (iuused_at)
Signature = Header/Payload를 조합하고, 비밀키로 서명한후, base64 인코딩
exp
: 토큰 만료 시간 (expiration)
aud
: 토큰 대상자 (audience)
iss
: 토큰 발급자 (issuer)
서버에서 토큰 발급 시에 SECRET_KEY
(비밀키)로 서명을 하고, 발급시간을 저장합니다.
SECRET_KEY
(비밀키) 서명은 암호화가 이니므로, 보안성 데이터는 넣지 말고 최소한의 필요한 정보만 넣도록 합니다. SECRET_KEY
에서 논의된 주의점은 데이터베이스 비밀번호, AWS 키, OAuth 토큰 등이 있습니다.
settings.SECRET_KEY
를 활용하거나, 별도의 JWT_SECRET_KEY
설정을 합니다.토큰에 담을 정보, 정보의 한 조각을 뜻하는 claim
이라는 key/value 형식의 exp 를 사용합니다.
claim
을 사용합니다.갱신 (Refresh) 메커니즘을 지원합니다. Token 유효기간 내에 갱신하거나, username/password 를 통해 재인증을 요구합니다.
이미 발급된 Token 을 폐기 (Revoke) 하는 것은 불가능합니다.
""" JWT Example """
>> from bse64 import b64decode
>> b64decode('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9')
b'{"typ":"JWT","alg":"HS256"}'
>> b64decode('eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFza2RqYW5nbyIsImV4cCI6MTUxNTcyMTIxMSwiZW1haWwiOiIifQ==')
b'{"user_id":1,"username":"askdjango","exp":1515721211,"email":""}'
🚀 (Django) SECRET_KEY
🚀 (JWT) Introduction
JWT는 만료시간이 있고, Refresh를 지원합니다.
일반 Token 인지 JWT Token 인지의 여부에 상관없이 Token 들은 반드시 안전하게 보관되어야 합니다. 스마트폰 앱의 경우 설치된 앱 별로 안전한 저장공간이 제공되지만, 웹브라우저에서는 그런 저장공간이 없습니다.
DRF에서 JWT를 기본으로 제공하지 않기때문에, simplejwt
서드파티 라이브러리를 이용합니다. 가이드 라인을 참고하여 이용해보겠습니다.
$ pip install djangorestframework-simplejwt
패키지 및 url을 등록합니다.
# settings.py
INSTALLED_APPS = [
...
'rest_framework_simplejwt',
]
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES' : [
...
'rest_framework_simplejwt.authentication.JWTAuthentication',
]
}
# accounts/urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView
urlpatterns = [
path('api-jwt-auth/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api-jwt-auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api-jwt-auth/verify/', TokenVerifyView.as_view(), name='token_verify'),
]
HTTPie 를 이용하여 POST 하여 JWT Token 을 발급 받습니다.
>> http POST http://localhost:8000/accounts/api-jwt-auth/ username="username password="password
""" 인증에 성공할 경우, 토큰 응답이 옵니다. """
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjQ0NjU2NjcyLCJpYXQiOjE2NDQ2NTYzNzIsImp0aSI6IjRjM2Y4Yzc3NmJhNDQ0MjRiNGFjYmVhOWMyYmMyNGQ3IiwidXNlcl9pZCI6MX0.720rWqd1MJD8Gc_0Xr2OgqercCIa0d6q1AnchDL3M6s",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY0NDc0Mjc3MiwiaWF0IjoxNjQ0NjU2MzcyLCJqdGkiOiI3YmQ0MjU1MjlhMTE0ZWNjYjZhZmVhMzM0ODMwZTMzMSIsInVzZXJfaWQiOjF9.gAjTKuVY91PZwL5dlLP04KfDlXvwk8-e6sP45Y2ujjM"
}
404 Bad Request
응답을 받습니다.HTTPie 를 이용하여 발급받은 JWT Token 을 확인합니다.
>> http http://localhost:8000/accounts/api-jwt-auth/verify/ token="token"
Signature has expired
응답을 받습니다.# djangorestframework-jwt
>> http http://localhost:8000/app/post "Authorization: JWT {{토큰}}"
# djangorestframework-simplejwt
>> http http://localhost:8000/app/post "Authorization: Bearer {{토큰}}"
>> http http://localhost:8000/accounts/api-jwt-auth/verify/ token="token"
HTTP/1.0 401 Unauthorized
{
"detail": "Signature has expired."
}
유효기간이 지났다면, 유효기간 내에 갱신을 해야만 합니다.
Signature has expired
응답을 받습니다.settings.JWT_AUTH
의 JWT_EXPIRATION_DELTA
를 참조 하며 default 는 5분입니다.settings.JWT_AUTH
default 값은 False 인데, 이 상태에서 갱신을 요청하면 orig_iat 필드를 찾을 수 없다는 응답을 반환합니다. True로 설정해야 갱신이 가능하다고 합니다. >> http POST http://localhost:8000/accounts/api-jwt-auth/refresh/ token="token"
# settings.py
from datetime import timedelta
...
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': False,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'JWK_URL': None,
'LEEWAY': 0,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
'JTI_CLAIM': 'jti',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}