장고 카카오소셜 로그인 + JWT

qq·2023년 8월 7일

Django

목록 보기
1/3

전에 AllAuth를 이용하여 카카오소셜 로그인까지 구현을 했는데 JWT(Access, Refresh Token)발급 받는 로직을 짜지 못해서 추가로 구현해보았습니다

먼저

#Settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',

    
    'django.contrib.sites',
    
    'user',
    # 설치한 라이브러리
    'rest_framework.authtoken',
    'rest_framework_simplejwt',
    'dj_rest_auth',
    'dj_rest_auth.registration',

    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.kakao',
    'corsheaders',
    
    
]
# 사이트는 1개만 사용할 것이라고 명시
SITE_ID = 2

AUTH_USER_MODEL = 'user.User'

REST_USE_JWT = True

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

# 추가적인 JWT 설정, 다 쓸 필요는 없지만 혹시 몰라서 다 넣었다.
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': False,
    'UPDATE_LAST_LOGIN': False,

    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': 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),
}
#models.py
from django.db import models

# Create your models here.

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin

# 헬퍼 클래스
class UserManager(BaseUserManager):
    def create_user(self, email, password, **kwargs):
        if not email:
            raise ValueError('Users must have an email address')
        user = self.model(
            email=email,
        )
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email=None, password=None, **extra_fields):
        superuser = self.create_user(
            email=email,
            password=password,
        )
        
        superuser.is_staff = True
        superuser.is_superuser = True
        superuser.is_active = True
        
        superuser.save(using=self._db)
        return superuser

# AbstractBaseUser를 상속해서 유저 커스텀
class User(AbstractBaseUser, PermissionsMixin):
    
    email = models.EmailField(max_length=30, unique=True, null=False, blank=False)
    is_superuser = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

	# 헬퍼 클래스 사용
    objects = UserManager()

	# 사용자의 username field는 email으로 설정 (이메일로 로그인)
    USERNAME_FIELD = 'email'
#views.py
from json import JSONDecodeError
from django.http import JsonResponse
from django.shortcuts import render
from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.kakao import views as kakao_view
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
import requests
from allauth.socialaccount.models import SocialAccount
from rest_framework import status
import json

from user.models import User

# Create your views here.
def kakao_callback(request):
    client_id = "6589033212a382a6c5ae48fbd88350aa"
    #code = request.GET.get("code")
    print(json.loads(request.body))
    data =json.loads(request.body)
    code = data.get('code')
    print(code)

    # code로 access token 요청
    token_request = requests.get(f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={client_id}&client_secret=Azywmif8MviNwe5gRMAvQrC91XH5SxRY&redirect_uri=http://localhost:3000/auth/code&code={code}")
    token_response_json = token_request.json()
    print(token_response_json)

    token_request.raise_for_status();

    access_token = token_response_json.get("access_token")
    
    # access token으로 카카오톡 프로필 요청
    profile_request = requests.post(
        "https://kapi.kakao.com/v2/user/me",
        headers={"Authorization": f"Bearer {access_token}"},
    )
    profile_json = profile_request.json()

    kakao_account = profile_json.get("kakao_account")
    email = kakao_account.get("email", None) # 이메일!

    # 이메일 없으면 오류 => 카카오톡 최신 버전에서는 이메일 없이 가입 가능해서 추후 수정해야함
    if email is None:
        return JsonResponse({'err_msg': 'failed to get email'}, status= status.HTTP_400_BAD_REQUEST)
     # 3. 전달받은 이메일, access_token, code를 바탕으로 회원가입/로그인
    try:
        # 전달받은 이메일로 등록된 유저가 있는지 탐색
        user = User.objects.get(email=email)

        # FK로 연결되어 있는 socialaccount 테이블에서 해당 이메일의 유저가 있는지 확인
        social_user = SocialAccount.objects.get(user=user)

        # 있는데 구글계정이 아니어도 에러
        if social_user.provider != 'kakao':
            return JsonResponse({'err_msg': 'no matching social type'}, status=status.HTTP_400_BAD_REQUEST)

        # 이미 Google로 제대로 가입된 유저 => 로그인 & 해당 우저의 jwt 발급
        data = {'access_token': access_token, 'code': code}
        accept = requests.post("http://localhost:8000/auth/finish/", data=data)
        accept_status = accept.status_code
        print(accept.json)
      
        # 뭔가 중간에 문제가 생기면 에러
        if accept_status != 200:
            return JsonResponse({'err_msg': 'failed to signin'}, status=accept_status)

        accept_json = accept.json()
        accept_json.pop('user', None)
        return JsonResponse(accept_json)

    except User.DoesNotExist:
        # 전달받은 이메일로 기존에 가입된 유저가 아예 없으면 => 새로 회원가입 & 해당 유저의 jwt 발급
        data = {'access_token': access_token, 'code': code}
        accept = requests.post("http://localhost:8000/auth/finish/", data=data)
        accept_status = accept.status_code

        # 뭔가 중간에 문제가 생기면 에러
        if accept_status != 200:
            return JsonResponse({'err_msg': 'failed to signup'}, status=accept_status)

        accept_json = accept.json()
        accept_json.pop('user', None)
        return JsonResponse(accept_json)
        
class KakaoLogin(SocialLoginView):
    adapter_class = kakao_view.KakaoOAuth2Adapter
    callback_url = 	"http://localhost:8000/auth/code"
    client_class = OAuth2Client

JWT 도입을 위해 이제 수정할 코드는

#Settings.py
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}
# Settings.py
def kakao_callback(request):
    client_id = "6589033212a382a6c5ae48fbd88350aa"
    #code = request.GET.get("code")
    print(json.loads(request.body))
    data =json.loads(request.body)
    code = data.get('code')
    print(code)

    # code로 access token 요청
    token_request = requests.get(f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={client_id}&client_secret=Azywmif8MviNwe5gRMAvQrC91XH5SxRY&redirect_uri=http://localhost:3000/auth/code&code={code}")
    token_response_json = token_request.json()
    print(token_response_json)

    token_request.raise_for_status();

    access_token = token_response_json.get("access_token")
    
    # access token으로 카카오톡 프로필 요청
    profile_request = requests.post(
        "https://kapi.kakao.com/v2/user/me",
        headers={"Authorization": f"Bearer {access_token}"},
    )
    profile_json = profile_request.json()

    kakao_account = profile_json.get("kakao_account")
    email = kakao_account.get("email", None) # 이메일!

    # 이메일 없으면 오류 => 카카오톡 최신 버전에서는 이메일 없이 가입 가능해서 추후 수정해야함
    if email is None:
        return JsonResponse({'err_msg': 'failed to get email'}, status= status.HTTP_400_BAD_REQUEST)
     # 3. 전달받은 이메일, access_token, code를 바탕으로 회원가입/로그인
    try:
        # 전달받은 이메일로 등록된 유저가 있는지 탐색
        user = User.objects.get(email=email)

        # FK로 연결되어 있는 socialaccount 테이블에서 해당 이메일의 유저가 있는지 확인
        social_user = SocialAccount.objects.get(user=user)

        # 있는데 카카오계정이 아니어도 에러
        if social_user.provider != 'kakao':
            return JsonResponse({'err_msg': 'no matching social type'}, status=status.HTTP_400_BAD_REQUEST)

        # 이미 Kakao로 제대로 가입된 유저 => 로그인 & 해당 유저의 jwt 발급
        data = {'access_token': access_token, 'code': code}
        accept = requests.post("http://localhost:8000/auth/finish/", data=data)
        accept_status = accept.status_code
        print(accept.json)
        
        access_token = AccessToken.for_user(user)
        refresh_token = RefreshToken.for_user(user)
        res = Response(
                    {
                    "message": "login success",
                    "token": {
                        "access": str(access_token),
                        "refresh": str(refresh_token),
                    },
                },
                status=200,
            )
        res.set_cookie('access', str(access_token), httponly=True)
        res.set_cookie('refresh', str(refresh_token), httponly=True)
      
        # 뭔가 중간에 문제가 생기면 에러
        if accept_status != 200:
            return JsonResponse({'err_msg': 'failed to signin'}, status=accept_status)

        accept_json = accept.json()
        accept_json.pop('user', None)
        print(res.data)
        return JsonResponse(res.data)

    except User.DoesNotExist:
        # 전달받은 이메일로 기존에 가입된 유저가 아예 없으면 => 새로 회원가입 & 해당 유저의 jwt 발급
        data = {'access_token': access_token, 'code': code}
        accept = requests.post("http://localhost:8000/auth/finish/", data=data)
        accept_status = accept.status_code

        # 뭔가 중간에 문제가 생기면 에러
        if accept_status != 200:
            return JsonResponse({'err_msg': 'failed to signup'}, status=accept_status)
        access_token = AccessToken.for_user(user)
        refresh_token = RefreshToken.for_user(user)
        res = Response(
                    {
                    "message": "login success",
                    "token": {
                        "access": str(access_token),
                        "refresh": str(refresh_token),
                    },
                },
                status=200,
            )
        res.set_cookie('access', str(access_token), httponly=True)
        res.set_cookie('refresh', str(refresh_token), httponly=True)

        accept_json = accept.json()
        accept_json.pop('user', None)
        print(res.data)
        return JsonResponse(res.data)

카카오 로그인 후, 발급 받은 accessToken을 가지고 요청을 하면 서버에서는 그 요청으로 받은 JWT를 통해 인증이 되도록 구현을 했습니다. 확인하기 위해 User의 views.py에

@api_view(['GET'])
@permission_classes((IsAuthenticated, ))
@authentication_classes([JWTAuthentication])
def userInfo(request):
    # Bearer 토큰 추출
    token = None
    authorization_header = request.headers.get('Authorization')
    if authorization_header and authorization_header.startswith('Bearer '):
        token = authorization_header.split(' ')[1]
        
    if not token:
        return Response({'message': 'Access token not provided'}, status=status.HTTP_401_UNAUTHORIZED)
    
    try:
 # 토큰 검증
        payload = jwt.decode(token, 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)

    #except(jwt.exceptions.ExpiredSignatureError):
            # 토큰 만료 시 토큰 갱신
            # data = {'refresh': request.COOKIES.get('refresh', None)}
            # serializer = TokenRefreshSerializer(data=data)
            # if serializer.is_valid(raise_exception=True):
            #     access = serializer.data.get('access', None)
            #     refresh = serializer.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)

(토큰이 만료된것을 감지 후 Refresh로직은 통신할때 구체적으로 구현해볼 예정입니다)

Postman으로 검사한 결과창입니다.

profile
백엔드 개발자

0개의 댓글