카카오 로그인 기능을 적용해보려한다. 카카오 개발가이드를 토대로 기능을 작성해보자.

이번에는 카카오 로그인 기능을 추가해보자.
카카오 로그인 기능을 추가하는 과정은 크게 보면 아래와 같다.

해당 로그인 기능은 OAuth 2.0을 지원합니다. 다음은 카카오 플랫폼 서비스에서 제공하는 가장 일반적인 OAuth 인증의 과정입니다.
1. 사용자는 카카오계정으로 로그인 버튼을 클릭합니다.
2. 카카오톡 앱에 연결된 카카오계정의 자격정보(Credentials)를 통해 사용자를 인식합니다.
3. 자격정보가 올바르다면 사용자(Resource Owner)로부터 접근 자원에 대한 동의/허가를 얻습니다.
4. 위 3까지 성공적으로 수행되었다면 인증 코드(Authorization Code)가 발급됩니다. 해당 인증 코드는 Redirection URI를 기반으로 Third 앱에 전달됩니다.
5. Third 앱에서는 전달받은 인증 코드를 기반으로 사용자 토큰(Access Token, Refresh Token)을 요청하고 얻게 됩니다.
-kakao developers


카카오 개발자 등록 및 키 발급

먼저 KakaoDevelopers에 가서 개발자 등록을 하고, 앱(서비스)를 등록한 후 API 키를 발급 받는다. 카카오 로그인에는 아래의 REST API키를 사용한다. 아래의 키들은 원래는 공개하면 안되므로 외부에 노출되지 않게 유의하자.

그리고 나서 설정-사용자 관리탭에서 사용자 관리 활성화를 해주고, 서비스에서 사용자들에게 받고 싶은 정보를 설정하면 된다.(프로필 정보를 제외하고는 전부 선택사항으로 받을 수 있다.)


프론트엔드 파트

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

코드 받기

아래 개발 가이드에 나와있는대로 차근차근 진행해보자. 가이드에 따르면, 아래의 엔드포인트에 GET Request를 보내야하고, 여기 안에 client_idredirect_uri를 넣어줘야한다고 한다.


여기서 말하는 client_id는 앱 등록을 한 후 받은 REST API키이다. 그리고 redirect_uri에는 코드를 발급받은 후 리다이렉트 해야할 페이지 주소를 넣어야한다. redirect_uri는 [사용자관리-로그인 Redirect URI]에서 설정할 수 있다. 지금은 프론트엔드 페이지가 구현되어있지 않기 때문에 callback이라는 임시 경로를 사용했다.


코드는 아래와 같이 작성하면 된다. client_id 같은 경우는 외부에 공개되는 키가 아니므로 다른 곳에 저장해서 불러왔다.

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

잘 작동하는지 확인해보자. 브라우저에 지금의 KakaoSignInView가 작동할 수 있는 주소를 입력하면 아래와 같이 카카오 로그인 창이 뜬다.
주소 입력 -> http://127.0.0.1:8000/account/sign-in/kakao/

이 창에서 사용자가 카카오 계정으로 로그인하게 되면, 이전에 사용자관리에서 선택한 정보들에 대한 동의를 얻은 후 코드를 쿼리스트링에 담아 미리 설정해놓은 redirect_uri로 리다이렉트한다. 여기서 우린 코드를 얻어내야한다.

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

로그인을 하고 허용할 정보들에 대해 동의를 하고나면, 아래처럼 Page not found 창이 뜬다. 잘못된게 아니라 redirect_uri로 설정한 경로에 아직 아무 페이지도 없기 때문이다. 이 페이지를 자세히보면 우리가 받으려했던 code가 숨어있다.

이제 이 코드를 카카오에 보내 실제로 API를 호출할 수 있는 사용자 토큰을 받아오면 된다.


사용자 토큰 받기


위의 가이드를 따라서 카카오에 사용자 토큰을 요청해보자. redirect_uri를 통해 이동되는 callback 페이지에 대한 뷰를 작성했다.

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

class KakaoSignInCallbackView(View):
    def get(self, request):

        try:
            code = request.GET.get("code")                                       [1]
            client_id = KAKAO_KEY
            redirect_uri = "http://127.0.0.1:8000/account/sign-in/kakao/callback"
            
            token_request = requests.get(                                        [2]
                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()                                    [3]
            
            error = token_json.get("error",None)

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

            access_token = token_json.get("access_token")                        [4]

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

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

[ 1 ] : 먼저 redirect 페이지로 리다이렉트 되면서 함께 전달된 코드를 가져다써야한다. code = request.GET.get("code")를 통해 code를 가져올 수 있다. request.GET은 리퀘스트로 들어온 모든 GET 데이터 객체를 가지고온다. 프린트해서 보면 아래와 같기 때문에, get("code")code를 가져올 수 있다.

print(request.GET)
<QueryDict: {'code': ['pqdi5nzyG5PJpHLXT6kiClp0H95V6QkOnJk7mFwUP3WNStywb8RhThx68krH9IGBSrflxQo9dJgAAAFwVdoHhw']}>

[ 2 ] : 가이드에서 get요청으로 받길 원하는 정보인 grant_type, client_id, redirect_uri, code를 f-strings에 넣어 카카오로 보내준다.

[ 3 ] : 응답 받은 데이터를 json으로 받으면 아래와 같이 토큰을 받아온 것을 알 수 있다. 지금은 access_token만 활용하도록 하자. [4]의 형식으로 access_token을 가져온다.


백엔드

백엔드에서는 프론트에서 받은 카카오의 사용자 토큰을 이용해 카카오에 사용자 정보를 요청한다.

사용자 정보 요청

이제 사용자 토큰을 이용해 카카오에 사용자 정보를 요청해보자.

개발 가이드에 따르면, 위의 타겟으로 Bearer {access_token} 헤더를 담은 요청을 보내라고 되어있다. 요청을 보내면 아래와 같은 사용자 정보를 받을 수 있다.

            #------get kakaotalk profile info------#

            profile_request = requests.get(                               [1]
                "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)                      [2]
            kakao_id = profile_json.get("id")                             [3]

[ 1 ] : 헤더에 토큰을 담아 요청 보내기
[ 2 ] : 이메일 가져오기
[ 3 ] : 카카오 id 가져오기


카카오로부터 받는 Response 객체에서 받은 사용자 정보를 json 형태로 변환한 결과는 아래와 같다. 보기 편하게 칸은 띄워뒀다.

{'id': 0000000000, 'connected_at': '2020-02-16T12:33:37Z', 

'properties': {'nickname': '홍길동', 'profile_image': 'http://k.kakaocdn.net/dn/c6Ro5m/btqwYVl2S8O/~~~~.jpg', 'thumbnail_image': 'http://k.kakaocdn.net/dn/c6Ro5m/btqwYVl2S8O/~~~.jpg'}, 

'kakao_account': {'profile_needs_agreement': False, 'profile': {'nickname': '홍길동', 'thumbnail_image_url': 'http://k.kakaocdn.net/dn/c6Ro5m/btqwYVl2S8O/~~~.jpg', 'profile_image_url': 'http://k.kakaocdn.net/dn/c6Ro5m/btqwYVl2S8O/~~~.jpg'}, 'has_email': True, 'email_needs_agreement': False, 'is_email_valid': True, 'is_email_verified': True, 'email': '~~~@kakao.com'}}


이제 이 정보를 가지고 우리 서비스의 로그인 시스템에 적용시켜보자.


        if Account.objects.filter(kakao_id = kakao_id).exists():        [1]
            user = Account.objects.get(kakao_id = kakao_id)             [2]
            token = jwt.encode({"email" : email}, SECRET_KEY, algorithm = "HS256") [3]
            token = token.decode("utf-8")
            
            return JsonResponse({"token" : token}, status=200)          [4]

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

[ 1 ] : 지금 접속한 카카오 아이디가 데이터베이스에 존재하는지 확인
[ 2 ] : 존재하는 카카오 아이디를 가진 유저 객체를 가져옴
[ 3 ] : 이메일 정보로 access token 발행
[ 4 ] : 발행한 토큰을 응답
[ 5 ] : 지금 접속한 카카오 아이디가 데이터베이스에 없을 경우, 새로운 유저 정보를 생성하고, 토큰을 발행함

위의 과정을 잘 따라왔다면, 로그인에 성공한 후 아래의 액세스 토큰이 브라우저에 반환 됐을 것이다.

끝!


작성 코드

class KakaoSignInView(View):
    def get(self, request):
        client_id = KAKAO_KEY
        redirect_uri = "http://127.0.0.1:8000/account/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:
            code = request.GET.get("code")
            client_id = KAKAO_KEY
            redirect_uri = "http://127.0.0.1:8000/account/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()
            
            error = token_json.get("error",None)

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

            access_token = token_json.get("access_token")

            #------get kakaotalk profile info------#

            profile_request = requests.get(
                "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)
            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 Account.objects.filter(kakao_id = kakao_id).exists():
            user = Account.objects.get(kakao_id = kakao_id)
            token = jwt.encode({"email" : email}, SECRET_KEY, algorithm = "HS256")
            token = token.decode("utf-8")

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

        else :
            Account(
                kakao_id = kakao_id,
                email    = email,
            ).save()

            token = jwt.encode({"email" : email}, SECRET_KEY, algorithm = "HS256")
            token = token.decode("utf-8")

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

참고자료

profile
개발자가 되어가는 중

1개의 댓글

comment-user-thumbnail
2020년 4월 25일

안녕하세요! 안드로이드와 카카오 로그인 API를 연동하려합니다.
내 어플리케이션에 설정 칸이 안뜨는데 혹시 만들 때 뭔가 선택해야하나요?

답글 달기