DRF JWT 인증
2022. 7. 15. 17:23ㆍ강의 정리/Django REST Framework
반응형
전 강의에서 배운 Django의 기본 Token은 랜덤 문자열로 구성되어 있기에 Token을 통해서 어떤 유저인지 알 수 없고, Token 유효기간도 없어서 위험하다.
이러한 기본 Token의 단점을 JWT를 통해서 보완해보자.
Token 인증과 JWT 인증
DRF의 기본 Token
단순한 랜덤 문자열
- 각 User와 1:1 매칭
- 유효기간이 없습니다.
- Token을 받았을 때 어떤 유저의 Token인지 알 수 없음
>>> import binascii
>>> import os
>>> binascii.hexlify(os.urandom(20)).decode()
'ec90f85721dc5f75b6eec47d199e3476c301633f'
JWT
- 데이터베이스를 조회하지 않아도, 로직만으로 인증이 가능 -> JWT 만으로 JSON 형태로 유저 정보를 담고있음.
- 포맷 : "헤더.내용.서명"
- 서버에서 토큰 발급 시에 비밀키로 서명을 하고, 발급 시간및 원하는 정보를 저장 -> 비밀키는 절대 노출되어서는 안된다.
- 서명은 암호화가 아닙니다. 누구라도 열어볼 수 있기에, 보안성 데이터는 넣지 말고, 최소한의 필요한 정보만 넣기.
- Claim : 담는 정보의 한 조각. key/value형식
- djangorestframework-jwt에서는 Payload에 user_id, username, email 이름의 claim을 사용
- 위변조가 불가 → 비밀키를 소중히
- 장고에서는 settings.SECRET_KEY를 활용하거나, 별도로 JWT_SECRET_KEY 설정을 합니다.
- 갱신(Refresh) 메커니즘을 지원
- JWT는 만료시간이 있고, 갱신을 지원한다.
- Token 유효기간 내에 갱신하거나, usernames/password를 통해 재인증 (Token 유효기간 내에 갱신 시, username/password가 아닌 기존의 Token을 이용해 갱신이 가능)
- 이미 발급된 Token을 폐기(Revoke)하는것은 불가
Token과 JWT
일반 토큰 예시)
8df73dafbde4c669dc37a9ea7620434515b2cc43
JSON Web Token 예시)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFza2RqYW5nbyIsImV4cCI6MTUxNTcyMTIxMSwiZW1haWwiOiIifQ.Zf_o3S7Q7-cmUzLWlGEQE5s6XoMguf8SLcF-2VdokJQ
- Header(헤더)를 base64 인코딩
- Payload(내용)를 base64 인코딩
- Signature(서명) = Header/Payload를 조합하고, 비밀키로 서명한 후, base64 인코딩
Payload를 변조해서 서버로 보냄 → 서버에서는 Signature 와 Payload값이 일치하지 않으므로, 무결하지 않다고 판단 후 거부가 가능하다.
>>> from base64 import b64decode
>>> b64decode('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9')
b'{"typ":"JWT","alg":"HS256"}'
>>> b64decode('eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFza2RqYW5nbyIsImV4cCI6MTUxNTcyMTIxMSwiZW1haWwiOiIifQ==')
b'{"user_id":1,"username":"askdjango","exp":1515721211,"email":""}'
- JWT를 해독한 결과
- 첫번째 b는 헤더, 두번째 b는 내용이다.
Token은 안전한 장소에 보관하기
- 일반 Token / JWT 토큰 여부에 상관없습니다.
- 스마트폰 앱은, 설치된 앱 별로 안전한 저장공간이 제공되지만, 웹브라우저에는 없습니다.
- Token은 앱 환경에서만 권장하기도 합니다.
- 웹 클라이언트 환경에서는 세션 인증이 나은 선택일 수 있습니다. 단 장고/웹클라이언트가 같은 호스트명을 가져야 함.
- 통신은 필히 https !!!
- Let's Encrypt(https보급을 위해 등장한 무료 인증 기관)에서 SSL인증서를 발급받아 서비스하자
djangorestframwork-simplejwt
https://github.com/jazzband/djangorestframework-simplejwt
https://jiaaan90.tistory.com/152
https://medium.com/chanjongs-programming-diary/django-rest-framework-drf-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-jwt-%EA%B8%B0%EB%B0%98-authentication-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0-with-simplejwt-%EC%B4%88%EA%B8%B0-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85-1-e54c3ed2420c
설치
pip3 install djangorestframework-simplejwt
세팅
# 프로젝트/settings.py
from datetime import timedelta
INSTALLED_APPS = [
# ...
'rest_framework_simplejwt',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
# 'rest_framework.authentication.BasicAuthentication',
# 'rest_framework.authentication.SessionAuthentication',
# 'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication', # 추가
],
}
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),
}
# 프로젝트/urls.py
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView
)
urlpatterns = [
# ...
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
]
rest_framework_jwt의 뷰 구현
from rest_framework import generics, status
from rest_framework.response import Response
from . import serializers
from .authentication import AUTH_HEADER_TYPES
from .exceptions import InvalidToken, TokenError
class TokenViewBase(generics.GenericAPIView):
permission_classes = ()
authentication_classes = ()
serializer_class = None
www_authenticate_realm = 'api'
def get_authenticate_header(self, request):
return '{0} realm="{1}"'.format(
AUTH_HEADER_TYPES[0],
self.www_authenticate_realm,
)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
return Response(serializer.validated_data, status=status.HTTP_200_OK)
class TokenObtainPairView(TokenViewBase):
"""
Takes a set of user credentials and returns an access and refresh JSON web
token pair to prove the authentication of those credentials.
"""
serializer_class = serializers.TokenObtainPairSerializer
token_obtain_pair = TokenObtainPairView.as_view()
class TokenRefreshView(TokenViewBase):
"""
Takes a refresh type JSON web token and returns an access type JSON web
token if the refresh token is valid.
"""
serializer_class = serializers.TokenRefreshSerializer
token_refresh = TokenRefreshView.as_view()
class TokenObtainSlidingView(TokenViewBase):
"""
Takes a set of user credentials and returns a sliding JSON web token to
prove the authentication of those credentials.
"""
serializer_class = serializers.TokenObtainSlidingSerializer
token_obtain_sliding = TokenObtainSlidingView.as_view()
class TokenRefreshSlidingView(TokenViewBase):
"""
Takes a sliding JSON web token and returns a new, refreshed version if the
token's refresh period has not expired.
"""
serializer_class = serializers.TokenRefreshSlidingSerializer
token_refresh_sliding = TokenRefreshSlidingView.as_view()
class TokenVerifyView(TokenViewBase):
"""
Takes a token and indicates if it is valid. This view provides no
information about a token's fitness for a particular use.
"""
serializer_class = serializers.TokenVerifySerializer
token_verify = TokenVerifyView.as_view()
class TokenBlacklistView(TokenViewBase):
"""
Takes a token and blacklists it. Must be used with the
`rest_framework_simplejwt.token_blacklist` app installed.
"""
serializer_class = serializers.TokenBlacklistSerializer
token_blacklist = TokenBlacklistView.as_view()
# 위 코드들이 모두 정상 처리 시에 아래 포맷의 응답을 보냄
{
"token": token,
"user": user
}
HTTPie를 통한 JWT 발급
- 인증에 실패할 경우, "400 Bad Request" 응답
- 모든 연결에서는 서버와 HTTPS 통신을 권장
- 발급받은 JWT Token을 jwt.io 서비스를 통해 검증해보세요
쉘> http POST http://서비스주소/api-jwt-auth/ username="유저명" password="암호"
{
"token": "인증에 성공할 경우, 토큰응답이 옵니다."
}
쉘> http POST http://서비스주소/api-jwt-auth/verify/ token="토큰"
{
"token": "검증에 성공할 경우, 검증한 토큰 응답이 옵니다."
}
발급받은 JWT Token으로 포스팅 목록 API 요청
- DRF Token에서는 인증헤더 시작문자열로 Token을 썼지만, 이제 JWT 사용.
- 매 요청시마다 인증을 수행합니다.
쉘> http http://서비스주소/post/ "Authorization: Bearer {{토큰}}" # djangorestframework-simplejwt
JWT Token 유효시간이 지났다면?
- JWT Token 유효기간 내에 갱신을 해야만 합니다.
- 유효기간이 지난 Token은 아래와 같이 "401 Unauthorized" 응답
- 유효기간 내에는 Token만으로 갱신 가능
- 유효기간이 지나면 다시 username/password를 통해 인증받아야만 합니다.
- 유효기간
- settings.JWT_AUTH의 JWT_EXPIRATION_DELTA참조 → 디폴트 5분
쉘> http http://서비스주소/post/ "Authorization: Bearer 토큰"`
HTTP/1.0 401 Unauthorized
{
"detail": "Signature has expired."
}
JWT Token 갱신받기
- Token 유효기간 내에만 가능
- settings.JWT_AUTH의 JWT_ALLOW_REFRESH 설정은 디폴트 False
- True 설정에서만 갱신 지원. False인 경우에는 orig_iat 필드를 찾을 수 없다는 응답
쉘> http POST http://서비스주소/api/token/refresh/ refresh="토큰"
{
"token": "갱신받은 JWT 토큰"
}
반응형
'강의 정리 > Django REST Framework' 카테고리의 다른 글
DRF Token인증 적용하기 (0) | 2022.07.15 |
---|---|
DRF Pagination (0) | 2022.07.14 |
DRF Authentication과 Permission (0) | 2022.07.14 |
DRF Serializer를 통한 유효성 검사 및 저장 (0) | 2022.07.14 |
From과 Serializer 관점에서 DRF 비교 (0) | 2022.07.13 |