2차 프로젝트 Quest 101 - 카카오 소셜 로그인과 Unit Test

Hailey Park·2021년 12월 26일
0

2차 프로젝트

목록 보기
2/4

2차 프로젝트에서는 최근 많은 웹/앱에서 이용되고 있는 카카오 소셜 로그인의 구현을 담당하게 되었다.

"로그인/회원가입인데 뭐.." 라고 생각했다가 카카오 공식문서를 보고 프로세스를 이해하고 진행하는 것이 생각보다 어렵고 공식문서 내용의 분량이 많아 난항을 겪었다.

소셜 로그인을 진행하는 주체

client : 바로 소셜로그인을 사용하는 유저, 즉 서비스를 만드는 사람이다.
resource owner : 소셜로그인 기능을 제공하는 서비스를 사용하는 유저이다. 여기서 owner가 붙는 이유는, client가 받고자 하는 정보의 주인이기 때문이다.
resource server: 소셜로그인 기능을 제공하는 곳, 그리고 client가 받아야 하는 진짜 유저의 데이터를 가지고 있는 곳.

카카오 소셜 로그인 프로세스

Authorized Redirect URL : 소셜 서비스가 인증이 가능하도록 권한을 부여하는 과정에서 그 인증코드(Authorized Code)를 전달해줄 경로

  1. 소셜 로그인에 접근하기 위해 client를 resource server(카카오)에 등록한다. 이 때, redirect url을 설정한다.
  2. client가 카카오 서버로 인가 코드를 요청하여 발급받는다.
  3. 발급받은 인가 코드로 client가 카카오 서버로 엑세스 토큰, 리프레시 토큰을 요청하여 발급받는다.
  4. 발급받은 토큰으로 API를 호출하여 카카오 서버로부터 사용자의 정보를 받아온다.
  5. 사용자가 로그인을 하면 우리 서비스 자체 토큰을 발급하여, 사용자는 헤더에 카카오 토큰이 아닌 우리 서비스 자체 토큰을 가지고 있게 된다.

이번에 우리 팀은 1,2,3번 과정을 프론트엔드에서, 4,5번 과정을 백엔드에서 진행했다.
그래서 프론트엔드로부터 카카오 엑세스 토큰을 넘겨받아, 해당 토큰으로 카카오 API에서 사용자 정보를 받아왔다.

사실 프론트엔드가 어디부터 어디까지, 백엔드가 어디부터 어디까지 진행해야 한다는 것에 정답은 없고, 모두 프론트엔드에서 또는 모두 백엔드에서 진행할 수도 있다.

카카오 REST API TEST

프론트엔드와 역할을 분담한 후, 프론트엔드로부터 토큰을 넘겨받지 않아도 이것저것 백엔드에서 시도해 볼 수 있는 방법이 있다.
카카오에서는 API TEST를 제공하여 이 TEST에서 액세스 토큰을 발급해준다.

발급받은 이 토큰을 가지고 POSTMAN으로 서비스 내 다른 API들을 만들 때 여러 테스트를 해볼 수 있었다.

응답 예시

{3 items
"id":int123456789
"kakao_account":{14 items
"profile_needs_agreement":boolfalse
"profile":{4 items
"nickname":string"홍길동"
"thumbnail_image_url":string"http://yyy.kakao.com/.../img_110x110.jpg"
"profile_image_url":string"http://yyy.kakao.com/dn/.../img_640x640...."
"is_default_image":boolfalse
}
"email_needs_agreement":boolfalse
"is_email_valid":booltrue
"is_email_verified":booltrue
"email":string"sample@sample.com"
"name_needs_agreement":boolfalse
"name":string"홍길동"
"age_range_needs_agreement":boolfalse
"age_range":string"20~29"
"birthday_needs_agreement":boolfalse
"birthday":string"1130"
"gender_needs_agreement":boolfalse
"gender":string"female"
}
"properties":{5 items
"nickname":string"홍길동카톡"
"thumbnail_image":string"http://xxx.kakao.co.kr/.../aaa.jpg"
"profile_image":string"http://xxx.kakao.co.kr/.../bbb.jpg"
"custom_field1":string"23"
"custom_field2":string"여"
}
}

소셜 로그인 API 코드


import jwt, requests

from django.views import View
from django.http  import JsonResponse
from datetime     import datetime, timedelta

from quest101.settings import SECRET_KEY, ALGORITHM
from users.models      import User

class KakaoAPI:
    def __init__(self, access_token):
        self.kakao_token = access_token
        self.kakao_url = 'https://kapi.kakao.com/v2/user/me'

    def get_kakao_user(self):
        kakao_headers = {'Authorization' : f'Bearer {self.kakao_token}'}
        response = requests.get(self.kakao_url, headers=kakao_headers, timeout = 5)
        
        if response.json().get('code') == -401: 
                return JsonResponse({'message': 'INVALID KAKAO USER'}, status=400)
            
        return response.json()

class KakaoSignInView(View):
    def get(self, request):
        try:
            kakao_token = request.headers.get('Authorization', None)
            kakao_api   = KakaoAPI(kakao_token)
            kakao_user  = kakao_api.get_kakao_user()
            
            kakao_id      = kakao_user['id']
            user_name          = kakao_user['kakao_account']['profile']['nickname']
            email         = kakao_user['kakao_account']['email']
            user_profile_image = kakao_user['kakao_account']['profile']['thumbnail_image_url']
            
            user, created = User.objects.get_or_create(
                kakao_id = kakao_id,
                defaults = {'user_name'          : user_name,
                            'email'         : email,
                            'user_profile_image' : user_profile_image
                        }
            )
            
            status_code  = 201 if created else 200
        
            access_token = jwt.encode({'user_id' : user.id, 'exp' : datetime.utcnow() + timedelta(days=7)}, SECRET_KEY, ALGORITHM)
            
            return JsonResponse({'message':'SUCCESS', 'access_token' : access_token}, status = status_code)
        
        except KeyError:
            return JsonResponse({'message':'KEY_ERROR'}, status = 400)
        
        except User.DoesNotExist:
            return JsonResponse({'message':'INVALID_USER'}, status = 404)
        
        except jwt.ExpiredSignatureError:
            return JsonResponse({'message':'TOKEN_EXPIRED'}, status = 400)

소셜로그인 Unit Test

import json
from django.test   import TestCase, Client
from unittest.mock import patch, MagicMock

from users.models  import User

class KakaoLoginTest(TestCase):
    def setUp(self):
        user = User.objects.create(
            kakao_id      = "01924234",
            name          = "박정현",
            email         = "hailey@gmail.com",
            profile_image = "https://ifh.cc/g/ElNIU1.jpg",
            phone_number  = "01012341234",
        )

    def tearDown(self):
        User.objects.all().delete()
        
    @patch('users.views.requests')
    def test_kakaologinview_get_success(self, mocked_requests):
        client = Client()
        class MockedResponse:
            def json(self):
                return{
                    "id": 2008761000,
                    "connected_at": "2021-12-10T03:56:15Z",
                    "properties": {
                        "nickname": "박정현",
                        "profile_image": "https://ifh.cc/g/ElNIU1.jpg",
                        "thumbnail_image": "https://ifh.cc/g/ElNIU1.jpg"
                    },
                    "kakao_account": {
                        "profile_needs_agreement": False,
                        "profile": {
                            "nickname": "박정현",
                            "thumbnail_image_url": "https://ifh.cc/g/ElNIU1.jpg",
                            "profile_image_url": "https://ifh.cc/g/ElNIU1.jpg",
                            "is_default_image": False
                        },
                        "has_email": True,
                        "email_needs_agreement": False,
                        "is_email_valid": True,
                        "is_email_verified": True,
                        "email": "hailey@gmail.com",
                        "has_phone_number": True,
                        "phone_number_needs_agreement": False,
                        "phone_number": "+82 10-9109-5601",
                        "has_birthday": True,
                        "birthday_needs_agreement": True,
                        "has_gender": True,
                        "gender_needs_agreement": False,
                        "gender": "female",
                        "is_kakaotalk_user": True
                }
                }

        mocked_requests.get = MagicMock(return_value = MockedResponse())
        headers = {"HTTP_Authorization" : "가짜 access_token"}
        response = client.get("/users/kakaosignin", **headers)
        
        access_token = response.json()['access_token']
        
        self.assertEqual(response.json(),{'message':'SUCCESS','access_token': access_token})
        self.assertEqual(response.status_code, 200 | 201) 

Reference

https://hazel-developer.tistory.com/79

profile
I'm a deeply superficial person.

0개의 댓글