Django

[Django] DRF JWT 인증 방식을 이용한 회원가입 & 로그인 (3. 이메일 로그인/로그아웃)

오늘 할 일을 내일로 2023. 9. 1. 00:56
로그인

내가 진행한 프로젝트에서는 이메일과 비밀번호로 로그인을 진행하였다. 그래서 username이 아닌 email로 로그인 / 로그아웃을 구현해보았다.

 

 

일단 코드를 보기에 앞서, 로그인 과정은

email & password 입력 -> 회원가입한 user인지 인증 -> token 발급 -> token을 쿠키에 저장

순서로 진행된다.

 

 

로그인을 username이 아닌 email로 진행하기 때문에 회원가입한 user인지 인증하는 과정에서 기본 authenticate()이라는 로그인 인증 함수가 제대로 작동하지 않았다.

 

 

그래서 authentication() 함수를  custom하여 사용하였다. 

 

1. authentication() custom 하기 

accounts/backends.py 

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend


class EmailBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        try:
            user = UserModel.objects.get(email=username)
        except UserModel.DoesNotExist:
            return None
        else:
            if user.check_password(password):
                return user
        return None

위의 custom authentication() 함수를 사용하면, username에 이메일 값을 집어넣으면 로그인 인증이 제대로 진행된다. 

 

 

그리고 custom된 코드를 실행하기 위해 

settings.py

AUTHENTICATION_BACKENDS = [
    'accounts.backends.EmailBackend',
]

설정을 해준다.

 

 

* ERROR * 

user.check_password() 부분에서 오류가 발생하였었는데, 분명 제대로 된 비밀번호 값을 입력했는데 비밀번호가 옳지 않다는 것이었다.

 

debugging을 하면서 화면에 DB에 저장된 비밀번호와 내가 입력한 비밀번호를 비교해 보았을 때에도 같은 값이었는데 오류가 발생하였다. 

 

check_password() 라는 함수는 입력 받은 문자열과 '암호화된' 비밀번호를 비교하는 함수이다. 

회원가입을 구현할 때, bcrypt를 통해 비밀번호를 암호화 하지 않고 DB에 저장하였기 때문에 같은 값이라도 check_password()가 False 값을 반환하였다. 

 

bcrypt를 설치하고 새로운 user 데이터를 생성하여 암호화된 비밀번호를 DB에 저장한 후, 

그 user에 대하여 login을 진행하였더니, 문제 없이 진행되었다. 

 

 

2. login API 작성

accounts/views.py

    def post(self, request):
        # user 인증
        user = authenticate(
            username=request.data.get("email"), password=request.data.get("password")
        )

        # 회원가입한 user일 경우
        if user is not None:
            serializer = UserSerializer(user)
            # JWT 토큰
            token = TokenObtainPairSerializer.get_token(user)
            refresh_token = str(token)
            access_token = str(token.access_token)
            res = Response(
                {
                    "user": serializer.data,
                    "message": "Login Success",
                    "token": {
                        "access": access_token,
                        "refresh": refresh_token
                    },
                },
                status=status.HTTP_200_OK
            )
            # JWT 토큰을 Cookie에 저장
            res.set_cookie("access", access_token, httponly=True)
            res.set_cookie("refresh", refresh_token, httponly=True)
            return res
        else:
            return Response(status=status.HTTP_400_BAD_REQUEST)

TokenObtainPairSerializer.get_token을 통해 refresh token을 얻을 수 있다. 

 


로그아웃

로그아웃의 경우에는 로그인 구현보다 훨씬 간단하다. 

 

access token과 refresh token을 담은 쿠키를 삭제하면 된다. 

 

 

1. logout API 작성

accounts/views.py

    def delete(self, request):
        response = Response({
            "message": "Logout Success"
        }, status=status.HTTP_200_OK)
        response.delete_cookie("access")
        response.delete_cookie("refresh")
        return response

 

 


User 확인

로그인을 한 상태라면, 해당 user의 정보를 확인할 수 있어야 한다. 

 

로그인을 하면서 발급받은 access token을 통해 user 정보를 얻을 수 있다. 

 

 

accounts/views.py

    def get(self, request):
        try:
            access = request.COOKIES.get('access')
            payload = jwt.decode(access, SECRET_KEY, algorithms=['HS256'])
            pk = payload.get('user_id')
            user = get_object_or_404(User, pk=pk)
            serializer = UserSerializer(instance=user)
            return Response(serializer.data, status=status.HTTP_200_OK)
        # token이 만료되었을 때
        except(jwt.exceptions.ExpiredSignatureError):
            data = {'refresh': request.COOKIES.get('refresh', None)}
            serializer = TokenRefreshSerializer(data=data)
            if serializer.is_valid(raise_exception=True):
                access = serializer.validated_data.get('access', None)
                refresh = serializer.validated_data.get('refresh', None)
                payload = jwt.decode(access, SECRET_KEY, algorithms=['HS256'])
                pk = payload.get('user_id')
                user = get_object_or_404(User, pk=pk)
                serializer = UserSerializer(instance=user)
                res = Response(serializer.data, status=status.HTTP_200_OK)
                res.set_cookie('access', access)
                res.set_cookie('refresh', refresh)
                return res
            raise jwt.exceptions.InvalidTokenError
        # 사용 불가능한 토큰일 때
        except(jwt.exceptions.InvalidTokenError):
            return Response(status=status.HTTP_400_BAD_REQUEST)

access token이 유효한 경우, token을 decode하여 user의 정보를 받아온다. 

 

만약, 설정한 시간이 지나 token이 만료되었을 때는 refresh 토큰을 통해 access token을 재발급 받고 재발급 받은 token을 통해 user 정보를 받아온다. 


accounts/views.py 의 로그인/로그아웃 관련 API 코드 전체를 보면

class AuthAPIView(APIView):
    # 01 token에 따른 user 정보 가져오기
    def get(self, request):
        try:
            access = request.COOKIES.get('access')
            payload = jwt.decode(access, SECRET_KEY, algorithms=['HS256'])
            pk = payload.get('user_id')
            user = get_object_or_404(User, pk=pk)
            serializer = UserSerializer(instance=user)
            return Response(serializer.data, status=status.HTTP_200_OK)
        # token이 만료되었을 때
        except(jwt.exceptions.ExpiredSignatureError):
            data = {'refresh': request.COOKIES.get('refresh', None)}
            serializer = TokenRefreshSerializer(data=data)
            if serializer.is_valid(raise_exception=True):
                access = serializer.validated_data.get('access', None)
                refresh = serializer.validated_data.get('refresh', None)
                payload = jwt.decode(access, SECRET_KEY, algorithms=['HS256'])
                pk = payload.get('user_id')
                user = get_object_or_404(User, pk=pk)
                serializer = UserSerializer(instance=user)
                res = Response(serializer.data, status=status.HTTP_200_OK)
                res.set_cookie('access', access)
                res.set_cookie('refresh', refresh)
                return res
            raise jwt.exceptions.InvalidTokenError
        # 사용 불가능한 토큰일 때
        except(jwt.exceptions.InvalidTokenError):
            return Response(status=status.HTTP_400_BAD_REQUEST)

    # 01-02 이메일 로그인
    def post(self, request):
        # user 인증
        user = authenticate(
            username=request.data.get("email"), password=request.data.get("password")
        )

        # 회원가입한 user일 경우
        if user is not None:
            serializer = UserSerializer(user)
            # JWT 토큰
            token = TokenObtainPairSerializer.get_token(user)
            refresh_token = str(token)
            access_token = str(token.access_token)
            res = Response(
                {
                    "user": serializer.data,
                    "message": "Login Success",
                    "token": {
                        "access": access_token,
                        "refresh": refresh_token
                    },
                },
                status=status.HTTP_200_OK
            )
            # JWT 토큰을 Cookie에 저장
            res.set_cookie("access", access_token, httponly=True)
            res.set_cookie("refresh", refresh_token, httponly=True)
            return res
        else:
            return Response(status=status.HTTP_400_BAD_REQUEST)

    # 01-03 이메일 로그아웃
    def delete(self, request):
        response = Response({
            "message": "Logout Success"
        }, status=status.HTTP_200_OK)
        response.delete_cookie("access")
        response.delete_cookie("refresh")
        return response

 

사실 어떤 분께서 잘 정리해두신 자료를 보고 코드를 작성한 거라 크게 어려움은 없었지만, 예상치 못한 곳에서 오류가 많이 발생하여 시간이 오래 걸렸다. 

 

그래도 회원 관리 기능을 구현하면서, JWT, cookie 등 여러 개념에 대해 공부하게 되었다. 

 

jwt, cookie, oAuth, session등의 개념은 후에 다시 정리해 볼 예정이다. 

 

 

 

<참고>

- 전체 코드 참고

https://velog.io/@kjyeon1101/DRF-JWT-%EC%9D%B8%EC%A6%9D%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%EB%A1%9C%EA%B7%B8%EC%9D%B8

 

[DRF] JWT 인증을 사용한 회원가입&로그인

작년에 멋사에서 이음 프로젝트를 하면서 가장 절실하게 깨달았던 점.. 유저는 무조건 시작단계에 해놔야 한다. 다른거 다 만들고 유저를 만들거나 고치려고 하면 너무 복잡하게 얽혀있어서 공

velog.io

- custom authentication()

https://rahmanfadhil.com/django-login-with-email/

 

Enable Login with Email in Django – Rahman Fadhil

Django is currently my favorite framework for building web applications. Despite its simplicity and delightful development experience, one…

rahmanfadhil.com

https://djangocentral.com/authentication-using-an-email-address/

 

Authentication using an Email Address In Django

Django's built in authentication system uses username to identify a user when they log in. However, many websites and applications now allow users

djangocentral.com

- check_password()

https://stackoverflow.com/questions/55571170/django-check-password-always-returning-false

 

Django check_password() always returning False

I have an existing database that is used with NodeJs application, now same database will be used for new application that is built with Django. I have an user_account table, which stores user login

stackoverflow.com

https://codingdog.tistory.com/entry/django-setpassword-%ED%95%A8%EC%88%98%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B4%85%EC%8B%9C%EB%8B%A4

 

django set_password 함수에 대해 알아봅시다.

장고에서 유저의 비밀번호를 어떻게 설정해야 할까요? 비밀번호는 평문으로 저장되면 안 됩니다. 그렇기 때문에, set_password 라는 별도의 함수를 제공합니다. 이 글에서는 암호화 방식에 대한 상

codingdog.tistory.com

https://velog.io/@kimphysicsman/DRF-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EC%84%A4%EC%A0%95-makepassword

 

DRF - 비밀번호 설정 (make_password)

문자열을 암호화 시키는 기능을 가진 Django에서 제공하는 함수 django.contrib.auth.hashers.make_password문자열과 암호화된 비밀번호가 일치하는지 확인하는 함수django.contrib.auth.hashers.check_passwordA

velog.io