안녕하세요! 레오입니다🤗 오랜만에 비가 추적추적 오고 있습니다. 비가 온 뒤엔 보통 추워지기 마련이어서 겨울이 이제 시작되려는건지 걱정스럽네요🥶 오늘은 회원 관리 API 중 꽃이라고 할 수 있는 로그인 기능을 구현해보겠습니다! 로그인 기능을 개발하기 전에는 그저 아이디, 비밀번호 투다다닥 입력한 뒤 로그인 버튼만 누르면, 로그인이 되거나 또는 비밀번호 오류가 발생하는 매우 간단한 작업인 줄 알았습니다. 이렇게 2주가 넘게 걸리고 머리가 지끈지끈 할 줄은 몰랐네요🤕🤕 로그인을 주제로는 회원가입된 계정에 대한 자체 로그인 기능과 카카오 로그인 API 기능을 이용한 카카오 로그인 기능 두 편에 걸쳐 포스팅 하겠습니다!🚌🚌
코드를 작성하기 전에 프로젝트에서 사용하려는 JWT 인증 방식을 간단하게 설명하고자 합니다. HTTP 통신은 요청과 응답의 한 사이클을 끝내면 연결을 끊고 통신을 종료합니다. 즉 이전에 했던 통신에 대한 정보를 전혀 담지 않습니다. 따라서 사용자가 로그인을 통해 인증 절차를 마쳤더라도 그 다음 통신에서는, 예를 들어 마이페이지 조회를 요청하여도 HTTP는 사용자가 인증된 사용자인지 아닌지 모르고 관심조차 없습니다. 결국 사용자가 인증된 사용자라는 것을 알려줄만한 방법이 필요하고 이러한 방법으로 쿠키/세션 방식과 JWT 방식이 있습니다. 본 포스트에서는 로그인 기능 구현에 집중할 것이고, 인증 방식에 관하여는 추후에 자세하게 다룰 예정이므로 지금 당장 궁금하신 분들은 정보의 바다에서 찾아보는 것을 추천드립니다! 인증 방식은 네트워크에서 매우 중요한 이론이므로 꼭 숙지하여야 하는 내용입니당!!😉😉
먼저 DRF의 JWT 패키지를 설치하겠습니다!
pip3 install djangorestframework-jwt
djangorestframework-simplejwt 패키지도 있는데 이 패키지는 이미 만들어져있는 API에 유저 정보를 보내면 token을 반환하는 패키지이고 제가 사용할 djangorestframework-jwt는 유틸쪽을 더욱 활용하여 직접 만들고 수정할 수 있는 특징이 있습니다. 비유적으로 표현하자면 simplejwt는 가공된 식품, jwt는 많은 재료들이 모여있는 주방으로 생각할 수 있습니다. 그럼 이제부터 jwt 재료들이 모여있는 주방에서 요리를 시작하겠습니다👨🍳👨🍳
가장 먼저 settings.py에 코드를 작성하겠습니다.
### myproject/settings.py
from datetime import timedelta
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
'rest_framework.permissions.IsAuthenticated',
],
"DEFAULT_AUTHENTICATION_CLASSES": (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
)
}
JWT_AUTH = {
'JWT_ENCODE_HANDLER':
'rest_framework_jwt.utils.jwt_encode_handler',
'JWT_DECODE_HANDLER':
'rest_framework_jwt.utils.jwt_decode_handler',
'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',
'JWT_PAYLOAD_GET_USER_ID_HANDLER':
'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',
'JWT_RESPONSE_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_response_payload_handler',
'JWT_SECRET_KEY': 'SECRET_KEY',
'JWT_GET_USER_SECRET_KEY': None,
'JWT_PUBLIC_KEY': None,
'JWT_PRIVATE_KEY': None,
'JWT_ALGORITHM': 'HS256',
'JWT_VERIFY': True,
'JWT_VERIFY_EXPIRATION': True,
'JWT_LEEWAY': 0,
'JWT_EXPIRATION_DELTA': timedelta(hours=1),
'JWT_AUDIENCE': None,
'JWT_ISSUER': None,
'JWT_ALLOW_REFRESH': False,
'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=7),
'JWT_AUTH_HEADER_PREFIX': 'Bearer',
'JWT_AUTH_COOKIE': None,
}
세팅할 변수들이 많은데, 이번 포스트에서는 로그인에서 쓰인 변수만 다루겠습니다.
DEFAULT_PERMISSION_CLASSES
: serializer의 디폴트 permission class를 정의합니다. 이는 view의 permission class 변수를 정의하지 않을 시 참조됩니다. 'IsAuthenticated'는 인증된 사용자에게 요청을 허락하는 객체입니다.DEFAULT_AUTHENTICATION_CLASSES
: 인증을 어떤 방식으로 진행할지 정의합니다. JWT_ENCODE_HANDLER
: 인자로 받은 payload를 secret key 또는 public/private key 를 이용하여 인코딩을 진행합니다.JWR_PAYLOAD_HANDLER
: 유저의 정보를 이용하여 payload로 만들어주며 만들어진 payload는 딕셔너리 형식입니다. 위의 'JWT_ENCODE_HANDLER'와 'JWR_PAYLOAD_HANDLER'는 serializer.py에서 더욱 자세히 다루겠습니다!JWT_SECRET_KEY
: 사용할 시크릿 키를 정의합니다. 이전 포스트에서 환경변수로 설정했던 개인이 할당받은 SECRET_KEY를 사용합니다.JWT_ALGORITHM
: 사용할 알고리즘을 선택합니다.JWT_EXPIRATION_DELTA
: 만들어진 Token을 만료시킬 시간을 정합니다. 이 시간은 timestamp 형식으로 표현됩니다.작성할 코드가 많지만 대부분 정형화되어있는 코드들입니다. 여기서 refresh를 하거나 해싱 알고리즘을 다른 것으로 쓰거 싶거나 할 때 해당 부분만 수정하면 됩니다! 이제는 시리얼라이저 코드를 알아보겠습니다🚶🚶
#user/serializers.py
from .models import User
from rest_framework_jwt.settings import api_settings
from django.contrib.auth import authenticate
from django.contrib.auth.models import update_last_login
JWT_PAYLOAD_HANDLER = api_settings.JWT_PAYLOAD_HANDLER
JWT_ENCODE_HANDLER = api_settings.JWT_ENCODE_HANDLER
class UserLoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255)
password = serializers.CharField(max_length=128, write_only=True)
token = serializers.CharField(max_length=255, read_only=True)
def validate(self, data):
email = data.get("email", None)
password = data.get("password", None)
user = authenticate(email=email, password=password)
if user is None:
raise serializers.ValidationError(
"A user with this email and password is not found."
)
try:
payload = JWT_PAYLOAD_HANDLER(user)
payload['nickname'] = User.objects.get(email=user).nickname
jwt_token = JWT_ENCODE_HANDLER(payload)
update_last_login(None, user)
except User.DoesNotExist:
raise serializers.ValidationError(
"User with given email and password does not exists"
)
return {
'email': user.email,
'token': jwt_token
}
JWT_PAYLOAD_HANDLER
: settings.py에서 정의한 PAYLOAD HANDLER를 불러옵니다. 깃허브 문서를 보면서 어떻게 작동하는지 알아보겠습니다. JWT_ENCODE_HANDLER
:settings.py에서 정의한 ENCODE HANDLER를 불러옵니다. 이 또한 깃허브 문서를 보면서 어떻게 작동하는지 알아보겠습니다.email
, password
, token
: 필드를 정의합니다. email과 password는 CRUD 할 필요가 없기때문에 ModelSerializer 대신 기본 시리얼라이저를 상속받습니다(email과 password 일치 확인은 하단의 authenticate 메소드로 할 예정). authenticate
: 인자로 받은 email과 password를 이용하여 확인합니다. check_password 메소드로 해싱된 password와 일치하는지 확인해줍니다. user의 쿼리셋을 반환합니다.payload['nickname']
: payload에 user의 nickname에 대한 정보를 담고싶어서 커스터마이징 해보았습니다. get 메소드와 user의 email을 이용하여 nickname을 받아서 payload에 넣어주었습니다.update_last_login
: 인자로 받은 user의 'last_login' 필드를 현재 시간으로 update합니다. update_last_login 메소드는 다소 쉬운 편이기에 설명은 생략하고 이에 관한 문서 링크만 게시하겠습니다!👇👇지금까지 어려운 코드들을 작성하느라 수고많으셨습니당💯💯 JWT를 직접 생성해보려니까 여간 복잡한게 아니지만 이렇게 모든 과정을 직접 살펴보니 이해가 잘 되기도 하고 에러가 발생해도 잡아낼 수 있을 것 같은 자신감이 듭니다😝 이제 마무리를 짓기 위해 view와 url을 손보도록 하겠습니다!
from .serializers import UserRegistrationSerializer, UserLoginSerializer
class UserLoginView(GenericAPIView):
permission_classes = (AllowAny,)
serializer_class = UserLoginSerializer
def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
response = {
"success": "True",
"status_code": status.HTTP_200_OK,
"message": "User Logged in successfully",
"token": serializer.data['token'],
}
status_code = status.HTTP_200_OK
return Response(response, status=status_code)
is_valid()
: 앞서 만들었던 validate() 함수를 이용하여 validation을 진행합니다.serializer.data['token']
: is_valid()로 입력된 data의 token 키를 이용하여 token을 불러옵니다.from django.conf.urls import url
from django.urls import path
from .views import UserRegistrationView, UserLoginView
urlpatterns = [
path('signup/', UserRegistrationView.as_view()),
path('signin/', UserLoginView.as_view()),
]
이제 모든 코드의 작성이 완료되었습니다! runserver를 통해 서버를 열어서 테스트 해보겠습니당
view에서 작성한 return의 형태대로 응답이 나왔습니다. token의 길이가 상당한데 어떤 정보를 담고있는지도 확인해보겠습니다!
jwt 홈페이지에 접속하면 디코딩을 할 수 있는 칸이 있습니다. 여기에 반환된 token을 붙여 넣고 payload를 확인해보면 serializer에서 넣었던 정보 그대로 들어가 있는 것을 알 수 있습니다 ㅎㅎㅎ
처음에는 permission_classes를 지정하지 않았습니다. 그로 인해 seetings.py의 permission default 값인 IsAuthenticated가 작동되고 인증 에러를 반환하였습니다. 따라서 permission_classes를 AllowAny로 정의해주고 난 뒤 에러를 수정할 수 있었습니다.
가장 속썩었던 에러였습니다. 디버깅을 통해 authenticate에서 None을 반환하는 것을 우선 파악하였습니다. 이에 비밀번호가 일치하지 않는 것으로 판단하고 새로 계정을 만들어서 다시 시도하였는데 그대로였습니다. 분명 3초 전에 만든 계정인데,,,🐠🐠 그래서 authenticate를 파보기로 결정하고 코드 문서를 찾아보았습니다.
이렇게 5시간을 쥐잡듯이 뒤져서 알아낸 것은 is_active가 False이면 authenticate()를 쓸 수 없다,,
만들었던 LoginSerializer는 serializers.Serializer를 상속받았고, serializers.Serializer는 BaseSerializer를 상속받았습니다. 위의 is_valid는 BaseSerializer 클래스의 함수입니다.
이제 run_validation이 어떻게 동작하는지 알아보겠습니다.
이상으로 자체 로그인 기능 구현에 대해서 알아보았습니다. 워낙 길고 고려해야 할 것들이 많아서 코딩하는 시간도 오래 걸렸고 포스팅 작성하는 시간도 오래 걸렸네용💦💦 하지만 걸린 시간만큼 인증, 보안 등에 대해 얻은 것도 많았던 기능이었습니다 ㅎㅎ 다음 시간에는 카카오를 이용한 로그인 기능을 구현해 보겠습니다! 긴 글 읽어주셔서 감사합니다!!