2차 프로젝트에서는 최근 많은 웹/앱에서 이용되고 있는 카카오 소셜 로그인의 구현을 담당하게 되었다.
"로그인/회원가입인데 뭐.." 라고 생각했다가 카카오 공식문서를 보고 프로세스를 이해하고 진행하는 것이 생각보다 어렵고 공식문서 내용의 분량이 많아 난항을 겪었다.
client : 바로 소셜로그인을 사용하는 유저, 즉 서비스를 만드는 사람이다.
resource owner : 소셜로그인 기능을 제공하는 서비스를 사용하는 유저이다. 여기서 owner가 붙는 이유는, client가 받고자 하는 정보의 주인이기 때문이다.
resource server: 소셜로그인 기능을 제공하는 곳, 그리고 client가 받아야 하는 진짜 유저의 데이터를 가지고 있는 곳.
Authorized Redirect URL : 소셜 서비스가 인증이 가능하도록 권한을 부여하는 과정에서 그 인증코드(Authorized Code)를 전달해줄 경로
이번에 우리 팀은 1,2,3번 과정을 프론트엔드에서, 4,5번 과정을 백엔드에서 진행했다.
그래서 프론트엔드로부터 카카오 엑세스 토큰을 넘겨받아, 해당 토큰으로 카카오 API에서 사용자 정보를 받아왔다.
사실 프론트엔드가 어디부터 어디까지, 백엔드가 어디부터 어디까지 진행해야 한다는 것에 정답은 없고, 모두 프론트엔드에서 또는 모두 백엔드에서 진행할 수도 있다.
프론트엔드와 역할을 분담한 후, 프론트엔드로부터 토큰을 넘겨받지 않아도 이것저것 백엔드에서 시도해 볼 수 있는 방법이 있다.
카카오에서는 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"여"
}
}
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)
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)