[PROJECT] AIRBNB CLONING #3

김기현·2022년 3월 26일
1

project_airbnb

목록 보기
3/6
post-thumbnail

요즘 거의 모든 사이트에서는 소셜 로그인 기능을 사용하고 있습니다.

이번 프로젝트에서 중점적으로 다룰 사항은 소셜 로그인 API를 통해 회원가입 및 로그인을 진행하려 합니다.

다음은 Airbnb의 로그인 페이지입니다.

Airbnb는 페이스북, 구글, 애플 등의 소셜 로그인이 있습니다. 이번 프로젝트 때는 소셜로그인으로 카카오 로그인 API를 사용하려 합니다.

소셜 로그인을 하기 위해서는 OAuth의 개념을 이해하고 있어야 합니다.

OAuth2

1. 주요 용어

Authentication
인증, 접근 자격이 있는지 검증하는 단계를 말합니다.

Authorization
인가, 자원에 접근할 권한을 부여합니다. 인가가 완료되면 리소스 접근 권한이 담긴 Access Token이 클라이언트에게 부여됩니다.

Access Token
리소스 서버에게서 리소스 소유자의 보호된 자원을 획득할 때 사용되는 만료 기간이 있는 Token입니다.

Refresh Token
Access Token이 만료될 때 이를 갱신하기 위한 용도로 사용되는 Token입니다. 일반적으로 Access Token보다 만료기간이 깁니다.

2. Roles

OAuth2 프로토콜을 구성하는 4가지의 역할입니다.

Resource Owner
리소스 소유자 또는 사용자입니다. 해당 프로젝트에서는 Kakao가 해당되며 보호된 자원에 접근할 수 있는 자격을 부여하주는 주체입니다. OAuth2 프로토콜 흐름에서 클라이언트를 인증(Authorize)하는 역할을 수행합니다. 인증이 완료되면 권한 획득자격을 클라이언트에게 부여합니다.

Client
보호된 자원을 사용하려고 접근 요청하는 애플리케이션입니다. 해당 프로젝트에서의 클라이언트는 네집내집입니다.

Resource Server
사용자의 보호된 자원을 호스팅하는 서버입니다.
Authorization Server
권한 서버로, 인증 및 인가를 수행합니다. 클라이언트의 접근 자격을 확인하고 Access Token을 발급해 권한을 부여하는 역할을 수행합니다. 일반적으로 권한 서버가 리소스 소유자와 클라이언트 사이에서 중개 역할을 수행하게 됩니다.

3. Obtaining Authorization

OAuth2 프로토콜에서 다양한 클라이언트 환경에 적합하도록 4가지의 종류의 프로토콜을 제공하고 있습니다.

  • Authorization Code Grant│ 권한 부여 승인 코드 방식
  • Implicit Grant │ 암묵적 승인 방식
  • Resource Owner Password Credentials Grant │ 자원 소유자 자격증명 승인 방식
  • Client Credentials Grant │클라이언트 자격증명 승인 방식

해당 프로젝트에서는 권한 부여 승인코드 방식을 사용하며 구조는 다음과 같습니다.

4. API Parameter

주요 API Parameter는 다음과 같습니다.

client_id, client_secret
클라이언트 자격증명으로 클라이언트가 권한 서버에 등록하면 발급되며 권한 서버 연동 시 클라이언트의 검증에 사용됩니다.

redirect_url
권한 서버가 요청에 대한 응답을 보낼 url을 설정합니다.

state
CSRF 공격에 대비하기 위해 클라이언트가 권한 서버에 요청할 때 포함하는 임의의 문자열입니다.

grant_type
Access Token 획득 요청 시 포함되는 값으로 권한 부여 방식에 대한 설정입니다.

Social Login Process

해당 블로그에서 구체적으로 정리해두었습니다.
해당 블로그를 참고하였습니다.

Flow

1. 준비

Client(YourHomeIsMine)는 OAuth를 제공하는 Resource Server(Kakao)의 개발자 콘솔에 서비스를 등록하고 client_id와 client_secret을 발급합니다. 그리고 redirect_url을 등록합니다.

2. Get Authorization Code

Client는 client_id와 redirect url, 그리고 권한 목록(이메일 등)의 정보들 등록해 로그인 버튼을 생성합니다.
User가 권한 승인과 더불어 로그인에 성공하면 Resource Server는 위에서 입력한 redirect url로 authorization code를 전달합니다.
이때 authorization code는 사용자 정보에 접근할 수 없고, 사용자 정보에 접근할 수 있는 access_token을 획득하기 위해 사용합니다.

3. Get Access Token

Client는 정해진 시간(24시간 미만) 이내에 cliend_id, redirect url, authorization code와 client_secret을 포함한 요청을 Resource Server에 보내면 access_token을 획득할 수 있습니다.

4. Access 사용자 정보 및 Use API

Client는 access_token을 이용해 사용자가 권한을 허락한 정보에 접근합니다.

Front-End
프론트엔드는 위 과정 중 1~3번에 해당하는 작업을 진행해 access_token을 백엔드에 전달합니다.

Back-End
백엔드는 프론트엔드에서 전달받은 access_token을 이용해 Resource Server에 등록된 사용자의 정보(unique_index)를 가져옵니다.

위에서 언급한 unique index와 인증을 진행한 Resource Server의 종류(예 : kakao 등)의 종류를 user table에 저장해 사용자를 생성하고 식별합니다.

Backend에서 프론트엔드가 처리해야 할 과정을 모두 처리한다면 CORS에러가 발생합니다. 위의 참고 블로그에서 정리해두었습니다.

config/urls

urlpatterns = [
    path("users", include('users.urls')),
]

users/urls

urlpatterns = [
    path("users", include('users.urls')),
]

users/views

import requests, jwt, datetime

from django.views import View
from django.http  import JsonResponse

from users.models import User
from my_settings  import SECRET_KEY, ALGORITHM

필요한 라이브러리들을 import합니다.

class KakaoSignIn(View):
    def get(self, request):
        try:
        	# 프론트엔드가 요청 보낸 후 보내지는 토큰을 담음
            kakao_token = request.headers.get("Authorization")
            
            if kakao_token == None:
                return JsonResponse({"message":"INVALID_ACCESS_TOKEN"}, status=401)
            
            # Client는 access_token을 이용해 사용자가 권한을 허락한 정보에 접근
            profile_request = requests.get(
                "https://kapi.kakao.com/v2/user/me",
                headers = {"Authorization": f"Bearer {kakao_token}"},
                timeout = 2
            )      
  
            profile_json  = profile_request.json()
            email         = profile_json.get("kakao_account").get("email", None)
            nickname      = profile_json.get("properties").get("nickname")
            profile_image = profile_json.get("kakao_account").get("profile").get("profile_image_url", None)
            gender        = profile_json.get("kakao_account").get("gender", None)
            kakao_id      = profile_json.get("id")
            

프론트엔드가 Resource Server에 client_id와 redirect url, 그리고 권한 목록(이메일 등)의 정보들 등록한 후 요청을 보내면 Resource Server가 토큰을 발행해줍니다.

그 후 Client가 access_token을 활용해 사용자가 권한을 허락한 정보에 접근합니다.User의 정보에 접근을 하면 다음의 구조로 응답이 발생합니다. 요청에 대한 응답인 profile_requestjson 형식으로 변환한 후에 유저의 정보를 가져오고, 만약 없을 경우를 대비해 get("email", None)과 같이 None값을 받도록 처리하였습니다.

            user, is_created = User.objects.get_or_create(
                kakao_id      = kakao_id,
                defaults={
                    'email'         : email,
                    'nickname'      : nickname,
                    'profile_image' : profile_image,
                    'gender'        : gender
                }
            )

그 후 get_or_create를 활용해 구하고자 하는 객체가 존재한다면 객체를 얻고 객체가 존재하지 않으면 User을 생성합니다.

            payload = {
                'user_id' : user.id, 
                'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24)
            }    
            access_token = jwt.encode(payload, SECRET_KEY, ALGORITHM)
           
            results = {
                'email'         : email,
                'nickname'      : nickname,
                'profile_image' : profile_image,
                'gender'        : gender,
                'kakao_id'      : kakao_id
            }               
    
            return JsonResponse({
                    'message'   : 'SUCCESS',
                    'token'     : access_token,
                    'results'   : results
                }, status = 201)                                    

        except AttributeError:
            return JsonResponse({'message' : 'CANNOT_GET_ATTRIBUTE'}, status = 400)
            
        except jwt.ExpiredSignatureError:
            return JsonResponse({'message' : 'EXPIRED_TOKEN'}, status = 400) 

그 후 Client(YourHomeIsMine)에서 User를 확인하기 위한 용도로 인가 코드를 발행합니다.
그리고 회원이 get or created되었을 때의 결과를 Response해줍니다. 추가로 상황에 맞도록 적절한 예외 처리를 해줍니다.

Login Decorator

Client(YourHomeIsMine)가 Resource Server에게 요청을 보낸 후에 얻은 access_token을 활용해 User의 정보를 알아낸 다음 User를 Created하거나 Get하였습니다. 그 이후 Client에서 활동하는 사용자를 인가하기 위해 Client에서 access_token을 발급하고 인가합니다.

import jwt

from django.http    import JsonResponse

from my_settings    import SECRET_KEY, ALGORITHM
from users.models   import User

def login_decorator(func):
    def wrapper(self, request, *args, **kwargs):
        try: 
            if 'Authorization' not in request.headers:
                return JsonResponse({"message" : "NO AUTHORIZATION IN HEADER"}, status = 401)

            token        = request.headers.get("Authorization")          
            payload      = jwt.decode(token, SECRET_KEY, ALGORITHM)  
            request.user = User.objects.get(id=payload["id"])

            return func(self, request, *args, **kwargs)

        except User.DoesNotExist:                                           
            return JsonResponse({'message' : 'INVALID_USER'}, status=400)
        except jwt.exceptions.ExpiredSignatureError:
            return JsonResponse({"message" : "EXPIRED_TOKEN"}, status=401)
        except jwt.exceptions.DecodeError:                                     
            return JsonResponse({'message' : 'INVALID_TOKEN'}, status=400)
    return 

User의 인가가 필요한 API를 접근할 때 @login_decorator를 활용합니다.

Errors

관리자 설정 이슈

해당 uri로 로그인을 실행했는데 KOE006 이슈가 발생하였습니다.이는 Redirect URI를 제대로 등록해주지 않아서 생긴 오류였습니다!! 수정수정

UnboundLocalError

토큰을 처리하는 과정에서 지역변수 user가 함수 본문 내에서 할당되기 전에 일부 변수가 참조되었다고 합니다.
파이썬에서는 지역 네임스페이스, 전역 네임스페이스, 빌트인 네임스페이스가 있고 위 그림과 같은 관계를 가지고 있습니다. 전역 변수를 참조하여 지역 네임스페이스에 없을 경우 전역 네임스페이스를 참조해서 처리한다고 이해하고 있는데요!

그런데 지역 네임스페이스에 없기에 전역 네임스페이스에서 조회한다고 생각하는데 에러가 발생하였습니다. 전역 네임스페이스에서 참조할 때 '할당'의 의미는 조금 다른데요!

global 선언 없이 지역 네임스페이스의 공간의 로직을 짤 때, 모듈(함수)에서 전역 네임스페이스의 변수를 '할당'할 일이 생기면 전역변수를 할당 시점에 컴파일러가 로컬 변수로 간주하기 때문에 UnboundLocalError를 발생시킵니다.

TypeError

urlpattern을 잘못 작성하여서 발생한 오류였습니다.

urlpatterns = [path("/login/kakao", KakaoSignIn.as_view)]urlpatterns = [path("/login/kakao", KakaoSignIn.as_view())]로 수정하였습니다. 너무나 초보적인 실수...

CORS Error

소셜 로그인을 진행할 때 프론트엔드에서 진행해야 할 부분까지 백엔드에서 진행할 때 발생한 오류입니다. 해당 블로그에서 더 자세하게 다루었습니다.

profile
피자, 코드, 커피를 사랑하는 피코커

0개의 댓글