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

 

GitHub - jazzband/djangorestframework-simplejwt: A JSON Web Token authentication plugin for the Django REST Framework.

A JSON Web Token authentication plugin for the Django REST Framework. - GitHub - jazzband/djangorestframework-simplejwt: A JSON Web Token authentication plugin for the Django REST Framework.

github.com

https://jiaaan90.tistory.com/152

 

[DRF] JWT (simple jwt)

JWT (Json Web Token) 인증 개발 경험에 대해 적어 보려고 한다. 처음에 구현 했던 방식은 djangorestframework-jwt 를 사용 했었는데 https://pypi.org/project/djangorestframework-jwt/ djangorestframework-jw..

jiaaan90.tistory.com

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

 

Django-Rest-Framework(DRF)로 JWT 기반 Authentication 세팅하기(with simplejwt) — 초기 환경 세팅(1)

앞서 포스팅했던 소셜 로그인 구현에서 생각보다 많은 개발자 분들이 봐주신 덕분에 상위노출도 되어 기뻤지만, 이전 코드를 다시 보니 많이 부족하단 생각이 들었다. 특히 JWT 부분에서 이해력

medium.com

 

 

설치

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 토큰"
}

 

반응형