dj-rest-auth 파헤치기-login

Hyeon Soo·2024년 5월 21일

앞서 dj-rest-auth는 serializer가 입, 출력값의 검증외에도 수행하는 기능이 있다고 말씀드렸었는데, login과정에서도 마찬가지입니다. Login Serializer부터 확인 해보겠습니다.

Login Serializer

Login 요청을 보내면, 따로 커스텀하여 설정한 serializer가 없는한 입력검증은 다음의 클래스를 거치게 됩니다. 전체 메서드 가운데 일부만 가져오겠습니다.

class LoginSerializer(serializers.Serializer):
    username = serializers.CharField(required=False, allow_blank=True)
    email = serializers.EmailField(required=False, allow_blank=True)
    password = serializers.CharField(style={'input_type': 'password'})

    def authenticate(self, **kwargs):
        return authenticate(self.context['request'], **kwargs)
        
    def validate(self, attrs):
        username = attrs.get('username')
        email = attrs.get('email')
        password = attrs.get('password')
        user = self.get_auth_user(username, email, password)

        if not user:
            msg = _('Unable to log in with provided credentials.')
            raise exceptions.ValidationError(msg)

        self.validate_auth_user_status(user)

        if 'dj_rest_auth.registration' in settings.INSTALLED_APPS:
            self.validate_email_verification_status(user, email=email)

        attrs['user'] = user
        return attrs
    

기본적으로 username, email, password를 입력으로 받습니다. 이때 username과 email은 필수값은 아니지만, 둘 중 하나의 값은 반드시 들어오도록 처리하고 있습니다(원본 클래스의 get_auth_user 메서드 참조). 적절환 값이 모두 들어왔다면, django에 기본적으로 내장되어 있는 authenticate 메서드를 통해 해당 credential과 일치하는 user 객체가 존재하는지를 탐색합니다.

user 객체가 존재하고, 해당 객체가 활성화된 계정이라면, login serializer는 입력값과 이에 해당하는 user 객체를 view로 넘겨줍니다.

Login View

여기서는 메서드별로 설명할 내용들이 다르기 떄문에, 메서드 단위로 살펴보도록 하겠습니다.

class LoginView(GenericAPIView):

    permission_classes = (AllowAny,)
    serializer_class = api_settings.LOGIN_SERIALIZER
    throttle_scope = 'dj_rest_auth'

    user = None
    access_token = None
    token = None
    def post(self, request, *args, **kwargs):
        self.request = request
        self.serializer = self.get_serializer(data=self.request.data)
        self.serializer.is_valid(raise_exception=True)

        self.login()
        return self.get_response()
    
    def login(self):
        self.user = self.serializer.validated_data['user']
        token_model = get_token_model()

        if api_settings.USE_JWT:
            self.access_token, self.refresh_token = jwt_encode(self.user)
        elif token_model:
            self.token = api_settings.TOKEN_CREATOR(token_model, self.user, self.serializer)

        if api_settings.SESSION_LOGIN:
            self.process_login()

Login View의 post 메서드에 나온 것처럼 request가 도달하면 우선 body의 값들을 Login serializer를 통해 검증합니다. 이 검증을 통과한다면, 아래의 login메서드를 호출합니다. login 메서드에서는 user 객체를 기반으로, settings에 설정한 authentication 방식에 맞는 응답을 생성할 수 있도록 값들을 처리합니다.

USE_JWT 옵션이 활성화되어 있다면, jwt를 생성하고, built-in token을 이용하거나 session을 이용한다면 이에 맞게 값을 생성합니다.

	def get_response(self):
        serializer_class = self.get_response_serializer()

        if api_settings.USE_JWT:
            from rest_framework_simplejwt.settings import (
                api_settings as jwt_settings,
            )
            access_token_expiration = (timezone.now() + jwt_settings.ACCESS_TOKEN_LIFETIME)
            refresh_token_expiration = (timezone.now() + jwt_settings.REFRESH_TOKEN_LIFETIME)
            return_expiration_times = api_settings.JWT_AUTH_RETURN_EXPIRATION
            auth_httponly = api_settings.JWT_AUTH_HTTPONLY

            data = {
                'user': self.user,
                'access': self.access_token,
            }

            if not auth_httponly:
                data['refresh'] = self.refresh_token
            else:
                # Wasnt sure if the serializer needed this
                data['refresh'] = ""

            if return_expiration_times:
                data['access_expiration'] = access_token_expiration
                data['refresh_expiration'] = refresh_token_expiration

            serializer = serializer_class(
                instance=data,
                context=self.get_serializer_context(),
            )
        elif self.token:
            serializer = serializer_class(
                instance=self.token,
                context=self.get_serializer_context(),
            )
        else:
            return Response(status=status.HTTP_204_NO_CONTENT)

        response = Response(serializer.data, status=status.HTTP_200_OK)
        if api_settings.USE_JWT:
            from .jwt_auth import set_jwt_cookies
            set_jwt_cookies(response, self.access_token, self.refresh_token)
        return response

실질적인 응답이 생성되는 부분입니다. 맨 처음의 get_response_serializer 메서드는 settings에 설정된 인증 방식에 따라 serializer를 가져오는데, JWT라면 JWT 또는 JWT EXPIRATION serializer를 가져오고, 그렇지 않다면 Token serializer를 반환합니다. 즉, session과 build-in token은 비슷하게 처리됨을 알 수 있습니다.

이하로는 개별 serializer를 설정한 대로 응답을 작성하여 사용자에게 보냅니다.

Simple JWT 세팅과 serializer

JWT의 생성과 응답값의 형식은 dj-rest-auth의 serializer를 통해 제어하지만, 유효기간, refresh token의 사용 여부, blacklist 여부, 비밀키와 알고리즘 같은 세팅들은 simplejwt에 기반하여 설정해주어야 합니다.

이때, 아무런 세팅이 없다면 라이브러리에 default로 선언되어 있는 설정에 따라 동작합니다. 이는

https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html

에서 확인하실 수 있습니다. 유의해야할 설정값들 위주로 본다면

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
    "ROTATE_REFRESH_TOKENS": False,
    "BLACKLIST_AFTER_ROTATION": False,

    "ALGORITHM": "HS256",
    "SIGNING_KEY": settings.SECRET_KEY,

}

정도를 꼽을 수 있습니다. LIFETIME은 각 토큰의 유효기간을 설정합니다. ROTATE_REFRESH_TOKEN은 refresh 토큰을 사용하여 인증정보를 갱신할때, access_token만 반환할지 refresh token까지 포함하여 반환할지를 정합니다.
BLACKLIST_AFTER_ROTATION 은 한번 사용한 refresh token을 영구히 폐기하고자 할 때 쓰이는데, 이 값을 활성화한다면 refresh가 발생할 때 사용한 refresh token을 데이터베이스 테이블에 등록하여 재사용이 불가능하도록 합니다.
ALGORITHM과 SIGNING_KEY는 JWT의 서명을 생성할 때 쓰이는 값입니다.

settings에 필요한 설정에 기반하여, dj-rest-auth의 JWT serializer, JWT expiration serializer은 다음과 같이 구성되어 있습니다.

class JWTSerializer(serializers.Serializer):
    access = serializers.CharField()
    refresh = serializers.CharField()
    user = serializers.SerializerMethodField()

    def get_user(self, obj):
        JWTUserDetailsSerializer = api_settings.USER_DETAILS_SERIALIZER

        user_data = JWTUserDetailsSerializer(obj['user'], context=self.context).data
        return user_data


class JWTSerializerWithExpiration(JWTSerializer):
    access_expiration = serializers.DateTimeField()
    refresh_expiration = serializers.DateTimeField()

기본적으로 JWTSerializerWithExpiration은 JWT serializer를 상속하기 때문에, 응답값을 마음대로 정의하고 싶다면 경우에 따라 JWTSerializerWithExpiration만 커스텀하거나, 둘 모두를 커스텀할 필요가 있습니다. 예를 들어 serializer가 반환하고 있는 user 객체를 지우고 싶다면 근본적으로는 JWTSerializer를 커스텀한 후, 커스텀은 클래스를 상속하도록 JWTSerializerWithExpritation을 마찬가지로 커스텀해야합니다.

권장되는 것은 아니지만, 위의 serializer에 들어 있는 정보 이외에, 추가적인 정보를 들고 있을 필요가 생길 수 있는데 이는 serializer의 변수로 선언하고, serializermethodfield를 이용하여 메서드 선언해서 값을 넣어줄 수 있습니다.

0개의 댓글