Django 소셜 로그인

손성수·2023년 7월 6일
0
post-thumbnail

Oauth 인증 과정 이해 하기

용어 정리

  • Oauth
    인증을 위한 개방형 표준 프로토콜

  • Resource Server
    kakao,naver,google등 유저 데이터를 갖고 있는 Server

  • Redirect URI
    소셜로그인 과정을 거치고, 사용자가 이동될 URI

  • Scope
    Resource Server에 저장된 유저 데이터중,
    어떤 데이터를 제공 받을지의 범위
    이메일,프로필 이미지,닉네임 등등

  • Client
    우리가 만든 장고 프로젝트의 백엔드 서버를 지칭
    이 포스트 게시글에서는 편하게 Client를 Backend Server로 지칭합니다.

그림으로 이해하기







필자의 프로젝트에서 구현한 방식 안내

구현한 소셜 로그인

  • kakao
  • naver
  • google

프로젝트의 Server

  • 프론트 Server
    사용자에게 서비스 화면 제공
  • 백엔드 Server
    사용자에게 데이터 서비스 제공

구현 과정

  1. 프론트 화면에서 사용자가 소셜 로그인 버튼을 클릭

  2. 프론트에서 백엔드로 필요한 데이터 요청
    Client ID와 필요시 Secret KEY 까지 제공

  3. 프론트에서 Resource Server로 제공할
    백엔드에서 수집한 데이터와, 미리 준비된 Scope를 정리

  4. 사용자를 Resource Server로 안내

  5. Resource Server는 사용자에게 동의 화면을 제공

  6. Resource Server는 사용자에게 인가코드를 발급하고
    Redirect URI로 안내

  7. 프론트에서 사용자가 가져온 인가코드를 가지고
    백엔드 서버로 데이터 발송

  8. 백엔드 Server는 인가코드를 토대로 Resource Server에
    AccessToken을 요청

  9. Resource Server가 제공한 AccessToken을 토대로
    사용자의 데이터 요청
    프로필 이미지, 닉네임, 이메일 정보 등등

  10. 제공받은 데이터를 토대로 사용자를 회원가입 또는 로그인 과저을
    거치고 사용자에게 Access Token을 발급

구현 방식

별도의 라이브러리를 사용하지 않고
하드 코딩으로 구현



1. 카카오 소셜 로그인

사전 작업

카카오 개발자를 위한 서비스 서버

  • 제품 및 문서 보기를 통해 구현 방법에 관한 힌트를 얻을 수 있다.
  • 내 에플리케이션을 클릭하여 에플리케이션을 등록 한다.


  • 보는 순간 클릭하고 싶어 참을 수 없을 것만 같은
    애플리케이션 추가를 클릭 한다.


  • 적절한 로고와 요청하는 정보를 입력해 주자.
    사업자명이 없다면 적절한 이름을 지어주도록 하자.



  • 요약 정보에 REST API KEY를 확인할 수 있다.
  • 필자는 앱을 바로 삭제할 것이기때문에 노출 시켰지만.
    노출된다면 보안 사고가 발생하기 때문에
    절대로 노출되어선 안된다.


  • 소셜 로그인을 활성화 한다.


  • Open ID Connect를 활성화 한다.
  • 동의 화면을 미리보기를 클릭해서 사용자에게 어떤 동의화면이 보여질지
    미리 확인할 수 있다.


  • Redirect URI를 설정해주자.
    Redirect URI란 무엇인지 위에 설명했지만 다시 언급하자면
    사용자가 로그인 동의후 어디로 이동할지 결정할 경로라고 보면 된다.
    필자는 프론트 서버를 Redirect URI로 설정했지만.
    백엔드 서버로 지정 했다.
    대게 Call Back URI라고 하기도 한다.


  • 동의 항목을 추가로 구성해준다
    Scope, 추가로 제공 받을 데이터를 지정해준다.
    일부 데이터는 사업자 정보가 있어야 가능하다.


  • 만약 보완 사고 또는 API KEY 노출이 우려 된다면
    재발급 신청을 할 수 있다.


  • 사용하지 않을 에플리케이션은 삭제를 해주도록 하자.


  • 추가로 Dot env를 사용하여 환경변수를 관리하는데
    REST API KEY를 저장해 둔다.

소셜 로그인 버튼을 누른 사용자 처리

... 프론트의 javascript
export async function kakaoLoginAPI() {
	// 카카오 로그인

	// 백엔드 서버로부터 kakao API 발급 받기
	const response = await fetch(`${BACK_BASE_URL}/api/users/kakao/login/`, { method: 'GET' })
	const kakao_id = await response.json()
	// Resource server와 약속된 REDIRECT URI 설정
	const redirect_uri = REDIRECT_URI
	// 요청할 데이터 설정
	const scope = 'profile_nickname,profile_image,account_email'
	// 사용자를 Resource Server로 이동
	// Resource Server는 사용자를 Redirect URI로 안내
	window.location.href = `https://kauth.kakao.com/oauth/authorize?client_id=${kakao_id}&redirect_uri=${redirect_uri}&response_type=code&scope=${scope}`
}
... 백엔드의 python
KAKAO_API_KEY = os.environ.get('KAKAO_API_KEY')
class KakaoLogin(APIView):
    """
    카카오 소셜 로그인
    """

    def get(self, request):
        """
        사용자가 Resource server로 로그인 요청시 Client의 API 인증키 발급
        """
        return Response(KAKAO_API_KEY, status=status.HTTP_200_OK)

이해하기

  1. 프론트의 fetch API를 이용하여 백엔드 Server로부터
    REST API KEY를 발급 받는다.
  2. 발급 받은 API KEY와,redirect uri, scope를 지정해서
    Resource Server로 사용자를 안내한다.
  3. 더 자세히 알고 싶다면 공식 문서를 확인해 보자.
    관련 공식 문서 자료

Redirect

  • Resource Server가 제공한 동의화면을 사용자가 마주하고
    로그인을 하면 Resource Server는 사용자에게 인가 코드를 발급하고
    Redirect URI로 이동시킨다.
... 프론트의 javascript
const code = new URLSearchParams(window.location.search).get("code");
getKakaoToken("code")

async function getKakaoToken(kakao_code) {
    // Resource Server로부터 응답받은 accesstoken을 백엔드 서버로 발송
    const response = await fetch(`${BACK_BASE_URL}/api/users/kakao/login/`, {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({ code: kakao_code })
    });
    setLocalStorage(response);
	// pay load 및 access Token 저장
}

def post(self, request):
    auth_code = request.data.get("code")
    
>> 먼저 프론트에서 전달한 인가 코드를 변수에 저장해둔다.
kakao_token_api = "https://kauth.kakao.com/oauth/token"
data = {
  "grant_type": "authorization_code",
  "client_id": KAKAO_API_KEY,
  "redirect_uri": REDIRECT_URI,
  "code": auth_code,
}

>> 공식 문서에서 요구하는 데이터를
딕셔너리 형태로 저장해 둔다.
kakao_token = requests.post(
	kakao_token_api,
	headers={"Content-Type": "application/x-www-form-urlencoded"},
	data=data,
)
# resource server가 발급한 access_token 확인
access_token = kakao_token.json().get("access_token")

>> request를 이용하여 post 요청을 발송하고
반환 받은 access token을 저장한다.

user_data = requests.get(
	"https://kapi.kakao.com/v2/user/me",
	headers={
	"Authorization": f"Bearer {access_token}",
	"Content-type": "application/x-www-form-urlencoded;charset=utf-8",
	},
)

>> header에 사용자의 access token을 실어 get 요청을 보낸다.
사용자의 정보를 잘 불러왔다면 status 는 200번대
보통 redirect uri나 api key값이 잘 못 되었다면
401 인증 오류를 반환 받게 된다.
user_data = user_data.json()
try:
	data = {
	"profile_image": user_data.get("properties").get("profile_image"),
	"nickname": user_data.get("properties").get("nickname"),
	"email": user_data.get("kakao_account").get("email"),
	"login_type": "kakao",
	}
except AttributeError:
	return Response({"msg": "유효하지 않은 토큰입니다."}, status=status.HTTP_401_UNAUTHORIZED)

# 로그인 및 회원 가입
return SocialLogin(**data)

>> 반환 받은 사용자의 데이터 정보를 딕셔너리에 담아내었다.
이제 resource Server와 데이터 주고 받기는 끝났으며
전달받은 데이터로 사용자 데이터를 저장하고 token을 발급하면 된다.
SocialLogin이라는 함수에 인자값을 전달하고
데이터를 저장하는 로직을 구현했다.
이는 마지막에 소개해 보겠다.

  • 전체 코드
class KakaoLogin(APIView):
    """
    카카오 소셜 로그인
    """

    def get(self, request):
        """
        사용자가 Resource server로 로그인 요청시 Client의 API 인증키 발급
        """
        return Response(KAKAO_API_KEY, status=status.HTTP_200_OK)

    def post(self, request):
        """
        Resource Server가 발급해준 인가 코드 확인
        인가 코드를 이용한 Access Token 발급 받기
        Access Token을 이용하여 사용자가 동의한 추가 제공 정보들 발급 받기
        추가 제공 정보를 토대로 로그인 및 회원가입 진행
        """

        # Resource Server의 인가 코드 확인
        auth_code = request.data.get("code")

        # 인가 코드를 이용한 Token 발급 받기
        # Kakao Token 발급 양식
        # https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token-request-body
        kakao_token_api = "https://kauth.kakao.com/oauth/token"
        data = {
            "grant_type": "authorization_code",
            "client_id": KAKAO_API_KEY,
            "redirect_uri": REDIRECT_URI,
            "code": auth_code,
        }
        kakao_token = requests.post(
            kakao_token_api,
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            data=data,
        )
        # resource server가 발급한 access_token 확인
        access_token = kakao_token.json().get("access_token")

        # access token 을 이용하여 resource server로부터 사용자가 동의한 추가 데이터 사항 불러오기
        user_data = requests.get(
            "https://kapi.kakao.com/v2/user/me",
            headers={
                "Authorization": f"Bearer {access_token}",
                "Content-type": "application/x-www-form-urlencoded;charset=utf-8",
            },
        )
        # 데이터 직렬화
        user_data = user_data.json()
        try:
            data = {
                "profile_image": user_data.get("properties").get("profile_image"),
                "nickname": user_data.get("properties").get("nickname"),
                "email": user_data.get("kakao_account").get("email"),
                "login_type": "kakao",
            }
        except AttributeError:
            return Response({"msg": "유효하지 않은 토큰입니다."}, status=status.HTTP_401_UNAUTHORIZED)
        #  로그인 및 회원 가입
        return SocialLogin(**data)

.. 추후 이어서 작성

profile
더 노력하겠습니다

0개의 댓글