dj-rest-auth 파헤치기-registration

Hyeon Soo·2024년 5월 7일

django는 파이썬 기반의 웹 프레임워크 가운데 가장 많이 쓰이고 있고, 다른 프레임워크에 비해 오래되었습니다. 그만큼 기본으로 제공하는 기능도 많고, 프레임워크에 기반하여 동작하는 라이브러리도 많습니다. 이 가운데 django의 기본적인 디자인 패턴과 기능들에 기반하여 RESTful-API 작성에 특화된 클래스들을 추가한 것이 django-rest-framework라이브러리이고, 세션, 토큰, jwt 토큰 및 OAuth2.0을 이용하는데 특화된 클래스가 추가된 것이 django-allauth와 django-simplejwt 라이브러리 입니다. 그리고 이 모든 라이브러리들을 이용하여 RESTful-API 형식에 맞는 인증 및 인가 체계를 제공하는 라이브러리가 dj-rest-auth라 할 수 있습니다.

dj-rest-auth와 의존 패키지들을 설치하고, django settings의 INSTALLED_APP에 명시해주는 것만으로도 세션, 토큰, JWT, OAuth2.0 기반 인가 및 인증 전체를 처리해주는 API를 이용할 수 있습니다. 세부적으로는 이메일 인증을 통한 사용자 활성화, 이메일 기반 비밀번호 초기화, 관리자용 퍼미션 등을 기본으로 내장하고 있습니다. 그리고 이 모든 기능들은 일반적으로 인증 단계에서 요구되는 여러 기본 사항들-비밀번호의 암호화 저장, 활성화 이메일 주소의 유효기간 설정, SQL 주입 방어를 위한 입력값 처리 방법-등을 기본으로 사용하고 있기 때문에 비교적 안전합니다. 이에 더하여 추가로 기능을 삽입하고 싶다면 디폴트로 선언된 클래스들을 상속후, 필요한 메서드를 커스텀하면 적용이 가능합니다.
이 글에서는 OAuth2.0을 제외한 registration과 관련된 내용과, django-allauth에서 제공하는 DefaultAccountAdapter을 살펴보려고 합니다. 다음 글에서는 login과 관련된 내용을, 그 다음에는 비밀번호 변경 및 초기화와 관련된 클래스들을 다루어 볼 예정입니다. 원본 소스코드는 다음 주소에 있습니다.

https://github.com/iMerica/dj-rest-auth/tree/master

Registration-RegisterView

회원가입과 관련된 dj-rest-auth의 클래스로는 RegisterSerializer와 RegisterView를 들 수 있습니다. serializer 클래스는 restframework에서 제공하는 클래스로, 입력된 값의 타입 검증 및 캐스팅 및 직렬화를 수행합니다. 다만 dj-rest-auth 라이브러리에서 serializer들은 단순 입력값의 검증 뿐만 아니라 일정부분 비즈니스 로직에 해당하는 부분도 처리하고 있는 경우들이 있으며, RegisterSerializer 또한 마찬가지입니다.

이는 RegisterView가 제공하고 있는 메서드를 확인하면 쉽게 알 수 있습니다.

class RegisterView(CreateAPIView):
    serializer_class = api_settings.REGISTER_SERIALIZER
    permission_classes = api_settings.REGISTER_PERMISSION_CLASSES
    token_model = TokenModel
    throttle_scope = 'dj_rest_auth'

    @sensitive_post_parameters_m
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)

    def get_response_data(self, user):
        if allauth_account_settings.EMAIL_VERIFICATION == \
                allauth_account_settings.EmailVerificationMethod.MANDATORY:
            return {'detail': _('Verification e-mail sent.')}

        if api_settings.USE_JWT:
            data = {
                'user': user,
                'access': self.access_token,
                'refresh': self.refresh_token,
            }
            return api_settings.JWT_SERIALIZER(data, context=self.get_serializer_context()).data
        elif api_settings.SESSION_LOGIN:
            return None
        else:
            return api_settings.TOKEN_SERIALIZER(user.auth_token, context=self.get_serializer_context()).data

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        data = self.get_response_data(user)

        if data:
            response = Response(
                data,
                status=status.HTTP_201_CREATED,
                headers=headers,
            )
        else:
            response = Response(status=status.HTTP_204_NO_CONTENT, headers=headers)

        return response

    def perform_create(self, serializer):
        user = serializer.save(self.request)
        if allauth_account_settings.EMAIL_VERIFICATION != \
                allauth_account_settings.EmailVerificationMethod.MANDATORY:
            if api_settings.USE_JWT:
                self.access_token, self.refresh_token = jwt_encode(user)
            elif not api_settings.SESSION_LOGIN:
                # Session authentication isn't active either, so this has to be
                #  token authentication
                api_settings.TOKEN_CREATOR(self.token_model, user, serializer)

        complete_signup(
            self.request._request, user,
            allauth_account_settings.EMAIL_VERIFICATION,
            None,
        )
        return user

주목해야할 메서드는 create 메서드입니다. 이 View는 restframework의 CreateAPIView를 상속하고 있기 때문에, POST request가 들어오면 바로 create 메서드를 실행합니다. 이 메서드는 serializer로 validation을 수행한 다음에, 유효한 값이 들어왔다면 하단의 perform_create 메서드를 실행합니다. 그리고 필요한 헤더와 응답을 생성하고 반환을 합니다. 그런데 perform_create를 보면, serializer의 save메서드를 실행해서 사용자를 획득합니다.

즉, RegisterView에서 가장 중요한 값의 검증과 사용자의 생성은 serializer에서 수행하고 있음을 알 수 있습니다. 그렇기 때문에, dj-rest-auth를 사용하며 회원가입을 커스텀하고 싶다면, RegisterSerializer의 메서드를 수정하는 것이 더 나은 방법임을 알 수 있습니다.

만약, serializer의 기능을 최대한 값의 검증으로만 한정하고 주요 기능을 view에 몰아넣고 싶다면 serializer의 기능들을 view에 최대한 추가하는 것으로 의도를 달성할 수 있습니다.

Registration-RegisterSerializer

앞서 살펴본 것처럼, 사용자 입력값의 검증에 더하여 실질적인 생성도 serializer를 통해 수행됩니다. 원본 소스코드를 통해 보면

class RegisterSerializer(serializers.Serializer):
    username = serializers.CharField(
        max_length=get_username_max_length(),
        min_length=allauth_account_settings.USERNAME_MIN_LENGTH,
        required=allauth_account_settings.USERNAME_REQUIRED,
    )
    email = serializers.EmailField(required=allauth_account_settings.EMAIL_REQUIRED)
    password1 = serializers.CharField(write_only=True)
    password2 = serializers.CharField(write_only=True)

    def validate_username(self, username):
        username = get_adapter().clean_username(username)
        return username

    def validate_email(self, email):
        email = get_adapter().clean_email(email)
        if allauth_account_settings.UNIQUE_EMAIL:
            if email and EmailAddress.objects.is_verified(email):
                raise serializers.ValidationError(
                    _('A user is already registered with this e-mail address.'),
                )
        return email

    def validate_password1(self, password):
        return get_adapter().clean_password(password)

    def validate(self, data):
        if data['password1'] != data['password2']:
            raise serializers.ValidationError(_("The two password fields didn't match."))
        return data

    def custom_signup(self, request, user):
        pass

    def get_cleaned_data(self):
        return {
            'username': self.validated_data.get('username', ''),
            'password1': self.validated_data.get('password1', ''),
            'email': self.validated_data.get('email', ''),
        }

    def save(self, request):
        adapter = get_adapter()
        user = adapter.new_user(request)
        self.cleaned_data = self.get_cleaned_data()
        user = adapter.save_user(request, user, self, commit=False)
        if "password1" in self.cleaned_data:
            try:
                adapter.clean_password(self.cleaned_data['password1'], user=user)
            except DjangoValidationError as exc:
                raise serializers.ValidationError(
                    detail=serializers.as_serializer_error(exc)
                )
        user.save()
        self.custom_signup(request, user)
        setup_user_email(request, user, [])
        return user

registration api가 호출될 경우, 별도의 세팅을 따로하지 않으면 위의 serializer가 호출되어 입력과 사용자 저장 등의 기능을 수행합니다. 만약 제공하고자하는 서비스에 따라 입력 값과 내부 동작을 바꾸어야한다면, 위의 serializer를 상속하는 serializer를 선언한 뒤, settings의 REST_AUTH 안에 'REGISTER_SERIALIZER'키의 값을 새로 선언한 serializer의 경로로 바꾸어주면 됩니다.

RegisterSerializer에서 중요한 것은 클래스의 변수, validate 메서드, save메서드입니다. 차례대로 살펴보겠습니다.

디폴트로 선언된 변수를 통해 username, email, password1, password2를 입력받는 것을 알 수 있습니다. username, email의 경우 settings의 USERNAME_REQUIRED, EMAIL_REQUIRED 값을 변경하여 필수 값 여부를 정할 수 있으며, 입력받아야하는 값을 더해야하거나 아예 빼버리고 싶다면 그에 맞게 변수를 선언해주면 됩니다.

validate 메서드는 기본적으로 password1과 password2가 같은지의 여부를 검사합니다. 또한 username, password1, email에 대해 django 및 django-allauth에서 기본적으로 제공하는 validation을 수행합니다. 만약 기본적으로 수행하는 validation외에 추가로 validation을 적용하고 싶다면, validate 메서드에 내용을 추가하면 됩니다. 예를 들어 패스워드에 영문 대소문자, 특수문자 등을 반드시 포함해야한다는 제약을 추가하려고하면, 필요한 정규식 패턴과 정규식 검증과정을 추가하고, 이에 맞게 ValidationError를 발생시키면 됩니다.

마지막으로 save메서드는 django-allauth의 adapter를 통해 사용자를 저장하고 view에 사용자 객체를 반환합니다. 이때 이메일을 이용한 사용자 활성화 기능을 사용하고 있다면 adapter의 수행과정에서 활성화용 이메일을 보내도록 되어 있습니다.

부록: DefaultAccountAdapter

앞서 밝힌 것처럼, dj-rest-auth는 django-rest-framework와 django-allauth를 기반으로 동작합니다. 특히 dj-rest-auth는 RESTful API 형식으로 외부와 연결될 수 있도록 클래스 구성을 한 것이 주된 목적이라, 내부적인 처리 및 동작은 상당부분 django-allauth 혹은 django의 auth에 선언되어 있는 기능들을 그대로 이용하고 있는 경우가 많습니다. 그 가운데 가장 많이 쓰이는 것이 DefaultAccountAdapter입니다.

이 클래스를 통해 dj-rest-auth는 사용자의 등록은 물론, 실질적인 authentication과 개별 오류 케이스를 모두 수행합니다. 또한 이메일을 통한 활성화 기능을 사용하고 있을 경우, adapter내부의 send_confirmation_mail 메서드를 통해 이메일을 보내고, 활성화 여부도 여기서 판단합니다.

기본적으로는 DefaultAccountAdapter가 get_adapter 함수를 통해 반환되지만, adapter가 수행하는 기능을 커스텀하고 싶다면 상속받은 클래스의 메서드를 수정한 다음에, settings의 ACCOUNT_ADAPTER의 값으로 커스텀 adapter의 경로를 작성해주면 됩니다.

0개의 댓글