Published on

DjangoRestFramework에서 JWT 사용하기

Authors

djangorestframework-simplejwt 를 이용해 Django Rest Framework 에서 JWT(Json Web Token)를 어떻게 사용하는지 기록합니다.

그 과정에서 세션대신에 왜 JWT를 사용하는지 생각해보고, JWT의 구조, Django Rest Framework에서의 설정 방법 및 주요 JWT 클레임 등을 알 수 있습니다.

1. JWT(Json Web Token)는 구조

JWT는 다음과 같은 세 부분으로 구성됩니다

  1. 헤더 (Header)
  2. 페이로드 (Payload)
  3. 서명 (Signature)

이 3가지 정보를 점(.) 으로 구분해서 하나의 문자열로 결합합니다.

예를 들어, eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZ~~~~

이런 식으로 보여집니다.

1.1 헤더 (Header)

헤더는 JWT의 유형과 서명 알고리즘을 정의합니다. 일반적으로 다음과 같은 JSON 객체로 구성됩니다.


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

1.2 페이로드 (Payload)

페이로드는 클레임(claims) 이라고 불리는 아래 정보의 조각들을 말하고 이를 갖고 있습니다.

{
  "token_type": "access",
  "exp": 만료 시간 (Unix 타임스탬프),
  "iat": 발급된 시간 (Unix 타임스탬프),
  "jti": JWT의 고유 식별자,
  "user_id": 사용자 ID"
}
  • exp (Expiration Time)

    • 토큰의 만료 시간을 나타냅니다. 이 시간이 지나면 토큰은 더 이상 유효하지 않습니다.
  • iat (Issued At)

    • 토큰이 발급된 시간을 나타냅니다. 이 정보는 주로 토큰의 유효성을 검증하는 데 사용됩니다.
  • jti (JWT ID)

    • JWT의 고유 식별자로, 주로 토큰을 추적하거나 블랙리스트와 같은 목록에서 식별하는 데 사용됩니다.
  • sub (Subject)

    • 토큰이 대표하는 주체(사용자나 어떤 서비스의 특정 ID등등..)를 나타냅니다.
  • aud (Audience)

    • 토큰을 사용할 수 있는 애플리케이션이나 비즈니스 로직에서 정하는 ID입니다.
  • nbf (Not Before)

    • 이 토큰이 사용 가능한 시점을 나타냅니다. 이 시점 이전에는 토큰이 유효하지 않습니다.

2. Django Rest Framework에서 JWT 사용하기

Django Rest Framework에서 JWT를 사용하려면, djangorestframework-simplejwt 패키지를 설치하고 설정해야 합니다.(하단에 공식문서 링크를 참고 하면 설치에 도움이 됩니다.)

2.1 세팅

  1. 패키지 설치
pip install djangorestframework-simplejwt
  1. 설정 추가

settings.py 에 JWT 설정을 추가합니다.

아래 기본설정만으로도 동작합니다.

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        '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": settings.SECRET_KEY,
    "VERIFYING_KEY": "",
    "AUDIENCE": None,
    "ISSUER": None,
    "JSON_ENCODER": 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),

    "TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
    "TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
    "TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
    "TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
    "SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
    "SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}



2.2 logic(feat. RefreshToken)

아래는 RefreshToken, BlacklistMixin 의 소스 코드로, 실무에서 주로 액세스 토큰과 리프레시 토큰을 사용하는 방식은 세션 기반 인증을 대체하거나 더 효율적으로 인증을 관리하기 편합니다.

  • 사용자가 로그인하면 access_token과 refresh_token을 함께 발급받습니다.

  • 액세스 토큰이 만료되었을 때, 사용자는 로그인하지 않고 리프레시 토큰을 사용해 새로운 액세스 토큰을 발급받습니다.

2.2.1 RefreshToken 소스 코드




class RefreshToken(BlacklistMixin, Token):
    token_type = "refresh"
    lifetime = api_settings.REFRESH_TOKEN_LIFETIME
    no_copy_claims = (
        api_settings.TOKEN_TYPE_CLAIM,
        "exp",
        # Both of these claims are included even though they may be the same.
        # It seems possible that a third party token might have a custom or
        # namespaced JTI claim as well as a default "jti" claim.  In that case,
        # we wouldn't want to copy either one.
        api_settings.JTI_CLAIM,
        "jti",
    )
    access_token_class = AccessToken

    @property
    def access_token(self):
        """
        Returns an access token created from this refresh token.  Copies all
        claims present in this refresh token to the new access token except
        those claims listed in the `no_copy_claims` attribute.
        """
        access = self.access_token_class()

        # Use instantiation time of refresh token as relative timestamp for
        # access token "exp" claim.  This ensures that both a refresh and
        # access token expire relative to the same time if they are created as
        # a pair.
        access.set_exp(from_time=self.current_time)

        no_copy = self.no_copy_claims
        for claim, value in self.payload.items():
            if claim in no_copy:
                continue
            access[claim] = value

        return access

  • 만약 로그아웃 처리를 원한다면, RefreshToken 클래스에서 제공하는 블랙리스트 기능을 사용할 수 있습니다. BlacklistMixin 클래스를 상속받았기 때문입니다. 주로 리프레시 토큰이 유효하지 않거나 로그아웃 시 블랙리스트 처리됩니다. 리프레시 토큰을 블랙리스트에 등록하면, 이후에는 해당 리프레시 토큰을 사용할 수 없습니다.

2.2.2 BlacklistMixin 소스코드

class BlacklistMixin:
    """
    If the `rest_framework_simplejwt.token_blacklist` app was configured to be
    used, tokens created from `BlacklistMixin` subclasses will insert
    themselves into an outstanding token list and also check for their
    membership in a token blacklist.
    """

    if "rest_framework_simplejwt.token_blacklist" in settings.INSTALLED_APPS:

        def verify(self, *args, **kwargs):
            self.check_blacklist()

            super().verify(*args, **kwargs)

        def check_blacklist(self):
            """
            Checks if this token is present in the token blacklist.  Raises
            `TokenError` if so.
            """
            jti = self.payload[api_settings.JTI_CLAIM]

            if BlacklistedToken.objects.filter(token__jti=jti).exists():
                raise TokenError(_("Token is blacklisted"))

        def blacklist(self):
            """
            Ensures this token is included in the outstanding token list and
            adds it to the blacklist.
            """
            jti = self.payload[api_settings.JTI_CLAIM]
            exp = self.payload["exp"]

            # Ensure outstanding token exists with given jti
            token, _ = OutstandingToken.objects.get_or_create(
                jti=jti,
                defaults={
                    "token": str(self),
                    "expires_at": datetime_from_epoch(exp),
                },
            )

            return BlacklistedToken.objects.get_or_create(token=token)

        @classmethod
        def for_user(cls, user):
            """
            Adds this token to the outstanding token list.
            """
            token = super().for_user(user)

            jti = token[api_settings.JTI_CLAIM]
            exp = token["exp"]

            OutstandingToken.objects.create(
                user=user,
                jti=jti,
                token=str(token),
                created_at=token.current_time,
                expires_at=datetime_from_epoch(exp),
            )

            return token

for_user 클래스 메서드는 주어진 사용자를 기반으로 새로운 토큰을 생성하고, 이를 데이터베이스에 저장하는 역할을 합니다.

JWT 토큰을 발급한 후 발급된 토큰을 데이터베이스에 기록하게 할 수 있으며,기본적으로는 블랙리스트 기능이 비활성화 되지만, 활성화하면 OutstandingToken 모델을 이용해 JWT 블랙리스트를 관리할 수 있습니다.

이렇게.. 발급된 모든 토큰을 추적할 수 있도록 하고, 필요에 따라 특정 토큰을 블랙리스트 처리하거나 관리할 수 있게 합니다.

3. 세션과 비교해서 생각해보는 JWT의 장점

3.1 상태 비저장 (Stateless)

JWT를 백엔드에서 프론트엔드(클라이언트)로 발급, 클라이언트는 JWT를 로컬에 저장합니다. 백엔드 입장에서는 클라이언트가 제대로 가지고있는지 확인하고 저장할 필요가 없습니다. 이는 곧 상태 비저장(stateless) 인증 방식이며 확장성과 유연성이 높아집니다.

반대로 세션 기반 인증은 서버에 세션 정보를 저장하기 때문에, 프론트엔드(클라이언트)가 요청을 보내면 서버는 세션을 조회해야 합니다.

그렇기 때문에 여러개의 데이터 베이스나 애플리케이션을 가지고 있는 구조라면 JWT 가 용이한 것입니다.

세션 기반 인증은 여러 서버에 세션 정보를 저장해야 하므로, 로드 밸런싱을 구현하는 것이 복잡해집니다. 모든 서버가 동일한 세션 스토리지를 사용하거나 세션 정보를 동기화 해야하기 때문입니다.

3.2 토큰 관리

클라이언트는 JWT를 로컬 저장소, 세션 ID를 쿠키에 저장할 수 있습니다.

JWT 를 사용하면 서버로부터의 요청 시 헤더값에 추가해서 매번 인증 정보를 포함하여 요청할 수 있고, (리프레시를 제외하고) 추가적인 요청 없이 인증 정보를 쉽게 사용할 수 있게 해줍니다.

세션을 쿠키에 저장하는 것 자체는 간단하지만, 세션 ID를 관리하기 위한 추가적인 작업이 필요하고 관리포인트가 늘어나는 것을 의미합니다..

3.3 통신관점

JWT는 HTTP 헤더에 쉽게 포함될 수 있습니다.

그렇기 때문에 다양한 클라이언트에서 인증을 간편하게 처리할 수 있습니다.

CORS(Cross-Origin Resource Sharing)로 자원 권한에 대한 비즈니스 로직이 필요한경우 유연하게 대처할 수 있습니다.

반면에 세션 쿠키는 CORS 설정에 따라 제약을 받을 수 있습니다. 수많은 다른 출처포인트에서 세션 쿠키가 올바르게 전송되지 않을 수도 있기 때문에 유의해야합니다.

4. 결론 for 세션 및 레퍼런스 링크

너무 JWT만 치켜세운 것 같은데, 세션도 매우 훌륭한 인증 처리 방식입니다.

JWT와 세션 모두 좋지만 사용되어지는 상황이 다를 수 있습니다.

세션의 경우 서버에서 관리가 가능하기 때문에, 보안성이 더 뛰어날 수 도있으며, 로그아웃이나 리프레시 처리에 대한 로직도 생길 수 있습니다.

  • hongreat 블로그의 글을 봐주셔서 감사합니다!^^
  • 내용에 잘못된 부분이나 의문점이 있으시다면 댓글 부탁 & 환영 합니다~!
  • (하단의 버튼을 누르시면 댓글을 보거나 작성할 수 있습니다.)
Buy Me A Coffee