카카오 소셜 로그인

.·2020년 7월 2일
13

Coding

목록 보기
33/33

1. 카카오개발자 등록, 키 발급

하기 사이트 참고
https://developers.kakao.com/

1-1. REST API 키

구글 검색 참고해서 카카오개발자 등록하게 되면 '내 애플리케이션' > '요약 정보' > '앱 키' 부분을 참고하면 네 종류의 키가 있다.
여기서 'REST API 키' 를 사용한다.

1-2. 동의항목 설정

카카오 로그인 ON 으로 설정 및 활성화 상태도 ON 으로 설정한다.
프로필 정보는 필수 동의로 설정해 두고 카카오계정(이메일) 은 선택동의로 설정해 놓는다.

1-3. 카카오 로그인 진행 과정


아래 카카오 로그인 진행과정은 사이트에서 퍼왔다.

  • 사용자가 앱에서 카카오 로그인 버튼을 클릭합니다.
  • 사용자가 '카카오톡으로 로그인'을 선택하면 카카오톡 실행 또는 실행 중인 카카오톡으로 연결되고, '다른 카카오계정으로 로그인'을 선택하면 직접 계정 정보를 입력하는 화면이 출력됩니다. 카카오톡이 설치되어 있지 않은 기기나 PC 웹 환경에서는 직접 계정 정보를 입력하여 카카오계정으로 로그인하게끔 진행됩니다.
  • '카카오톡으로 간편로그인'한 경우, 카카오톡에 연결된 카카오계정의 자격정보(Credentials)를 통해 사용자를 인식합니다. 직접 카카오계정을 입력해 로그인한 경우에는 해당 계정의 자격정보로 인식합니다.
  • 자격정보가 올바르다면 카카오 로그인 동의 화면을 통해 사용자로부터 사용자 정보 및 기능 활용 동의를 받습니다.
  • 사용자가 필수 항목에 동의하고 로그인을 요청하면 인증 코드(Authorization Code)가 발급됩니다. 이 코드는 앱 정보의 Redirect URI에 전달됩니다.
  • 앱은 전달 받은 인증 코드를 기반으로 사용자 토큰을 요청하고 받습니다.

카카오 로그인은 카카오계정의 사용자 자격정보로 인증 코드를 받아오고, 인증 코드로 액세스 토큰과 리프레스 토큰을 얻는 과정으로 돼 있습니다. 액세스 토큰은 사용자를 인증하고 카카오 API 호출 권한을 부여합니다. 리프레시 토큰(Refresh Token)은 사용자가 매번 카카오계정 정보를 입력하거나 카카오톡으로 로그인하지 않고도 액세스 토큰을 발급 받을 수 있게 합니다.

2. 프론트엔드

2-1. 인증 코드 받아오기

프론트엔드는 로그인 동의 화면을 호출하고, 로그인 버튼 클릭 시 사용자의 동의를 거쳐서 토큰을 받을 수 있는 코드를 받아와야 한다. 받은 인증 코드는 사용자 토큰 받기에 사용된다.
이 코드를 가지고 실제로 API를 호출할 수 있는 사용자 토큰(Access Token, Refresh Token)을 카카오로부터 받아올 수 있다.

사용자가 필수 동의 항목에 모두 동의한 뒤 '동의하고 시작하기' 버튼을 누르면, 카카오 인증 서버는 해당 사용자에 대한 인증 코드를 발급해 서비스의 redirect_uri에 전달하게 된다.

가이드를 확인해보면 카카오에 인증 코드를 요청하기 위한 엔드포인트(경로) 가 나와 있고 그 엔드포인트에 'app_key' 와 'redirect_url' 을 넣어 GET Request 를 보낸다.

GET /oauth/authorize?client_id={app_key}&redirect_uri={redirect_uri}&response_type=code HTTP/1.1
Host: kauth.kakao.com

parameter 의 'client_id' 는 설명에 나오듯이 앱 생성시 발급 받은 REST API 키 를 의미한다. 그리고 'redirect_uri' 에는 코드를 발급 받은 후 리다이렉트 해야 할 페이지 주소를 넣어줘야 한다. redirect_uri 는 아래 화면과 같이 미리 설정하여 놓는다.

인증 코드 요청의 응답은 redirect_uri로 HTTP 302 Redirect되며, Location에 인증 코드가 담긴 쿼리 스트링(Query String) 또는 에러 메시지를 포함한다. 사용자가 취소 버튼을 클릭한 경우에는 에러 메시지를 담은 쿼리 스트링이 redirect_uri로 전송된다.

서비스 서버는 redirect_uri로 받은 요청을 처리해 인증 코드를 얻거나 상황에 맞는 페이지를 보여주도록 처리해야 한다. 받은 인증 코드는 사용자 토큰 받기에 사용한다.

import json
import bcrypt
import jwt
import requests

from django.shortcuts import redirect
from django.views import View
from django.http import JsonResponse, HttpResponse
from django.core.exceptions import ObjectDoesNotExist

from my_settings import KAKAO_KEY, SECRET
from .models import Member

class KakaoSignInView(View):
    def get(self, request):
        client_id = KAKAO_KEY['KAKAO_KEY']
        redirect_uri = "http://127.0.0.1:8000/member/sign-in/kakao/callback"
        return redirect(
            f"https://kauth.kakao.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code"
        )

위와 같이 코드를 작성하였다. 'client_id' 의 경우에는 외부에 공개되면 안되는 키값이기 때문에 'KAKAO_KEY' 라는 변수 및 다른 파일 경로에 저장해서 불러오게 하였다.

이제 메인의 urls.py 를 수정해주고 member 앱의 urls.py 에 다음과 같이 코드를 작성해 넣었다.

from django.urls import path
from .views import KakaoSignInView

urlpatterns = [
    path('/sign-in/kakao', KakaoSignInView.as_view()),
]

이제 브라우저에 KakaoSignInView가 작동할 수 있는 주소를 입력하면 아래와 같이 카카오 로그인 창이 뜬다.

http://127.0.0.1:8000/account/sign-in/kakao/

이 곳에서 카카오 계정으로 로그인하면 [동의항목]에서 설정한 정보들에 대해서 동의를 얻고 코드를 쿼리스트링에 담아 미리 설정해놓은 redirect_uri로 리다이렉트한다. 이 때 우린 코드를 얻어야 한다.

만약 사용자가 동의를 하지 않으면 에러를 쿼리스트링에 담아 리다이렉트한다.

위의 화면처럼 나타난 창에서 로그인을 하고 동의를 체크하면 아래와 같이 'Page not found' 라고 뜬다. 왜냐하면 'redirect_uri' 로 설정한 경로에 아직 아무 페이지도 없기 때문이다.

위의 페이지 화면을 자세히 보면 Request URL 의 뒷 부분에 받고자 했던 code 가 들어 있다.
이제 이 코드를 카카오에 보내서 실제로 API 를 호출할 수 있는 사용자 토큰을 받아오게 하자.

2-2. 사용자 토큰 받기


아래의 샘플을 참고해서 토큰을 요청할 코드를 작성해 보자.
새로 작성하게 되는 class KakaoSignInCallbackView 는 redirect_uri 를 통해서 이동되는 callback 이라는 엔드포인트에 대한 코드(view)이다.

class KakaoSignInCallbackView(View):
    def get(self, request):
        try:
            #print(request.GET)
            code            = request.GET.get("code")
            client_id       = KAKAO_KEY['KAKAO_KEY']
            redirect_uri    = "http://127.0.0.1:8000/member/sign-in/kakao/callback"
            token_request   = requests.get(
                f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={client_id}&redirect_uri={redirect_uri}&code={code}"
            )
            token_json      = token_request.json()
            #print(token_json)

            error           = token_json.get("error", None)

            if error is not None:
                return JsonResponse({"message":"INVALD_CODE"}, status=400)

            access_token    = token_json.get("access_token")

            #return JsonResponse({'access_token':access_token}, status=200)         
            
            except KeyError:
            	return JsonResponse({"message":"INVALID_TOKEN"}, status=400)
                
            except access_token.DoesNotExist:
            	return JsonResponse({"message":"INVALID_TOKEN"}, status=400)


            

request.GET은 리퀘스트로 들어온 모든 GET 데이터 객체를 가지고 온다.
print(request.GET) 의 결과는 아래와 같다.

그러므로 code = request.GET.get("code") 를 통해 redirect 페이지로 리다이렉트 되면서 함께 전달된 코드를 가져올 수 있고 그 값을 사용하기 위해 code 라는 변수에 할당해 주었다.

token_request   = requests.get(
                f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={client_id}&redirect_uri={redirect_uri}&code={code}"

위와 같이 요청으로 받고자 하는 정보들을 f-strings 에 넣어 카카오로 보내준다.

token_request를 json으로 변환하여 token_json에 저장하고 print(token_json) 해보면 아래와 같다.

여기서 access_token 을 가져와서 일단 변수에 담아 놓는다.

member 앱의 urls.py 는 아래와 같이 수정한다.

from django.urls import path
from .views import KakaoSignInView, KakaoSignInCallbackView

urlpatterns = [
    path('/sign-in/kakao', KakaoSignInView.as_view()),
    path('/sign-in/kakao/callback', KakaoSignInCallbackView.as_view()),
]

3. 백엔드

3-1. 카카오에 사용자 정보 요청

프론트에서 받은 카카오의 사용자 토큰을 이용해 카카오에 사용자 정보를 요청한다.
https://developers.kakao.com/docs/latest/ko/user-mgmt/rest-api


가이드를 따라가면 access_token 을 이용한 사용자 정보 요청에 대한 설명이 있다.
Authorization 은 HTTP request의 헤더 부분에 있다. 즉, 헤더에 Bearer {access_token} 을 담아서 요청을 보내라는 의미이다.

작성한 위의 코드에서 access_token 과 아래 except KeyError 사이에 아래와 같은 코드를 입력해 준다.

            profile_request     = requests.get(
                "https://kapi.kakao.com/v2/user/me", headers={"Authorization" : f"Bearer {access_token}"},
            )

            profile_json        = profile_request.json()
            print(profile_json)
            kakao_account       = profile_json.get("kakao_account")
            email               = kakao_account.get("email", None)
            kakao_id            = profile_json.get("id")

카카오로 부터 받은 Response 객체에서 정보들을 json 형태로 변환한 결과(즉, profile_json)를 print문으로 뽑아 보면 아래와 같다. (개인정보 일부 변경함ㅠ)

{'id': 1000000000, 'connected_at': '2020-06-27T08:52:11Z',
'properties': {'nickname': '테디정', 
'profile_image': 'http://k.kakaocdn.net/dn/btdJyA/1000000000/1h1sox1000000000/img_640x640.jpg', 
'thumbnail_image': 'http://k.kakaocdn.net/dn/btdJyA/1000000000/1h1sox1000000000/img_110x110.jpg'}, 
'kakao_account': {'profile_needs_agreement': False, 'profile': {'nickname': '테디정', 
'thumbnail_image_url': 'http://k.kakaocdn.net/dn/btdJyA/1000000000/1h1sox1000000000/img_110x110.jpg', 
'profile_image_url': 'http://k.kakaocdn.net/dn/btdJyA/1000000000/1h1sox1000000000/img_640x640.jpg'}, 
'has_email': True, 'email_needs_agreement': False, 'is_email_valid': True, 'is_email_verified': True, 
'email': 'zzanggu@hotmail.com'}}

카카오로부터 아이디와 이메일 등의 정보들을 받았으니, 이제 위의 정보를 가지고 백엔드 서비스인 로그인 시스템에 적용시켜 보도록 한다.
아래의 코드는 위의 코드 문장 맨 아래부터 작성하도록 한다.

        if Member.objects.filter(social_account = kakao_id).exists():
            kakao_user    = Member.objects.get(social_account = kakao_id)
            token   = jwt.encode({"email":email}, SECRET['SECRET_KEY'], algorithm="HS256")
            token   = token.decode("utf-8")

            print("success")

            return JsonResponse({"token" : token}, status=200)

        else:
            Member(social_account = kakao_id,
                   email    = email,
            ).save()
            token = jwt.encode({"email" : email}, SECRET['SECRET_KEY'], algorithm = "HS256")
            token = token.decode("utf-8")
            return JsonResponse({"token" : token}, status = 200)

접속한 카카오 아이디가 데이터베이스에도 존재하는지 확인하고, 존재한다면 카카오 아이디를 갖고 있는 유저 객체를 갖고온다. 아이디 정보로 토큰을 발행하고, 토큰을 리턴해준다.
현재 접속한 카카오 아이디가 데이터베이스에 없다면 새로운 유저를 저장하고 토큰을 발행하게 한다.

4. 전체 코드

import json
import bcrypt
import jwt
import requests

from django.shortcuts import redirect
from django.views import View
from django.http import JsonResponse, HttpResponse
from django.core.exceptions import ObjectDoesNotExist

from my_settings import KAKAO_KEY, SECRET
from .models import Member

class KakaoSignInView(View):
    def get(self, request):
        client_id = KAKAO_KEY['KAKAO_KEY']
        redirect_uri = "http://127.0.0.1:8000/member/sign-in/kakao/callback"
        return redirect(
            f"https://kauth.kakao.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code"
        )

class KakaoSignInCallbackView(View):
    def get(self, request):
        try:
            #print(request.GET)
            code            = request.GET.get("code")
            client_id       = KAKAO_KEY['KAKAO_KEY']
            redirect_uri    = "http://127.0.0.1:8000/member/sign-in/kakao/callback"
            token_request   = requests.get(
                f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={client_id}&redirect_uri={redirect_uri}&code={code}"
            )
            token_json      = token_request.json()
            #print(token_json)

            error           = token_json.get("error", None)

            if error is not None:
                return JsonResponse({"message":"INVALD_CODE"}, status=400)

            access_token    = token_json.get("access_token")

            #return JsonResponse({'access_token':access_token}, status=200)

            profile_request     = requests.get(
                "https://kapi.kakao.com/v2/user/me", headers={"Authorization" : f"Bearer {access_token}"},
            )

            profile_json        = profile_request.json()
            print(profile_json)
            kakao_account       = profile_json.get("kakao_account")
            email               = kakao_account.get("email", None)
            kakao_id            = profile_json.get("id")

        except KeyError:
            return JsonResponse({"message":"INVALID_TOKEN"}, status=400)

        except access_token.DoesNotExist:
            return JsonResponse({"message":"INVALID_TOKEN"}, status=400)

        if Member.objects.filter(social_account = kakao_id).exists():
            kakao_user    = Member.objects.get(social_account = kakao_id)
            token   = jwt.encode({"email":email}, SECRET['SECRET_KEY'], algorithm="HS256")
            token   = token.decode("utf-8")

            print("success")

            return JsonResponse({"token" : token}, status=200)

        else:
            Member(social_account = kakao_id,
                   email    = email,
            ).save()
            token = jwt.encode({"email" : email}, SECRET['SECRET_KEY'], algorithm = "HS256")
            token = token.decode("utf-8")
            return JsonResponse({"token" : token}, status = 200)
profile
.

0개의 댓글