[OAuth] Django로 구현하는 구글 소셜로그인

우노·2024년 6월 23일
1

OAuth

목록 보기
4/4

🚧 해당 글은 Python으로 OAuth로 구글 소셜로그인을 위한 OpenAPI를 단순 호출하는 과정과 코드를 담았을 뿐, OAuth에 관한 내용은 이전 글을 참고해주세요!

사전준비

accounts.models.py에 User 모델 정의

  • AbstractBaseUser처럼 password가 없거나 필수가 아닌 모델
  • email, username 속성 포함

GCP 세팅하기

  1. GCP console에서 프로젝트 생성 - 이름은 아무거나

  2. API 및 서비스 > OAuth 동의 화면 > “외부” 선택 후 만들기

  3. 앱 정보 설정 - 앱 이름 맘대로 + 본인 이메일 입력
    일단 승인된 도메인 일단 없이 계속 진행
    → 로그인 테스트할 사용자 이메일 입력하기

  4. API 및 서비스 > 사용자 인증 정보 만들기 > OAuth 클라이언트 ID

  1. 웹 애플리케이션 선택 + 이름 맘대로 + 리디렉션 URI 추가하기
  1. 클라이언트 ID 비밀번호 확인

secrets.json

{
  "SECRET_KEY": ...,

  "GOOGLE_CLIENT_ID": ...,
  "GOOGLE_SECRET": ...,
  "GOOGLE_SCOPE_USERINFO": "https://www.googleapis.com/auth/userinfo.email",
  "GOOGLE_CALLBACK_URI": "http://localhost:8000/account/google/callback/",
  "GOOGLE_REDIRECT": "https://accounts.google.com/o/oauth2/v2/auth"
}

📎 scope(가져올 수 있는 데이터 목록)는 Google API의 OAuth 2.0 범위에서 확인 가능

Code

다른 어떤 방식으로라도 OAuth 구조만 지켜진다면 적절하게 OpenAPI 호출만 한다면 어떤 방식으로 구현해도 상관이 없습니다.

당연하게도 실습의 serializer, 함수/view 등을 꼭 따라야하는 법은 어디에도 없습니당

django-allauth 패키지 설치하기 → 기본 로그인 기능과 소셜로그인 테이블 지원

pip install django-allauth
pip install requests    # API 호출

config/settings.py 에 다음 내용 추가해주기

THIRD_PARTY_APPS = [
    # ...

    "allauth",
    "allauth.account",
    "allauth.socialaccount",
    "allauth.socialaccount.providers.google",    
    # "allauth.socialaccount.providers.{제공_업체}" 찾아서 사용 가능
]

# ...

MIDDLEWARE = [
		# ...
    "allauth.account.middleware.AccountMiddleware",  
]

# ...

ACCOUNT_EMAIL_REQUIRED = True            # email 필드 사용 o
ACCOUNT_USERNAME_REQUIRED = True         # username 필드 사용 o
ACCOUNT_AUTHENTICATION_METHOD = "email"

django-allauth 테이블 생성

python manage.py makemigrations    - 암것도 없다 그럼
python manage.py migrate           - 변경사항 와다다 뜸

THIRD_PARTY_APPS 영향으로 생성된 테이블 목록

account_emailaddress
account_emailconfirmation
socialaccount_socialaccount
socialaccount_socialapp
socialaccount_socialtoken

아래 테이블을 알잘딱깔센 활용 꼭 안 써도 돌아가긴 해서 알잘딱깔센..

  • (옵션) config/urls.py 에 엔드포인트 추가

    allauth가 기본 로그인 기능도 지원한다고 했죠.

    from django.contrib import admin
    from django.urls import path, include
    
    urlpatterns = [
        path("admin/", admin.site.urls),
        path("post/", include("posts.urls")),
        path("account/", include("accounts.urls")),
        path("account/", include("allauth.urls")),
    ]

    Note that you do not necessarily need the URLs provided by django.contrib.auth.urls. Instead of the URLs loginlogout, and password_change (among others), you can use the URLs provided by allauthaccount_loginaccount_logoutaccount_set_password

    대충 로그인 관련 API 지원한다는 소리입니다.

accounts/urls.py 에 엔드포인트 추가 → 이제부터 두 함수를 채워갈 겁니다.

urlpatterns = [
    # ...

    path("google/login/", google_login, name="google_login"),
    path("google/callback/", google_callback, name="google_callback"),
]

accounts/views.py 에 구현 추가

### --------
# secrets.json 읽어오기

from pathlib import Path
import os, json
from django.core.exceptions import ImproperlyConfigured

BASE_DIR = Path(__file__).resolve().parent.parent
secret_file = os.path.join(BASE_DIR, "secrets.json")

with open(secret_file) as f:
    secrets = json.loads(f.read())

def get_secret(setting, secrets=secrets):
    try:
        return secrets[setting]
    except KeyError:
        error_msg = "Set the {} environment variable".format(setting)
        raise ImproperlyConfigured(error_msg)

GOOGLE_SCOPE_USERINFO = get_secret("GOOGLE_SCOPE_USERINFO")
GOOGLE_REDIRECT = get_secret("GOOGLE_REDIRECT")
GOOGLE_CALLBACK_URI = get_secret("GOOGLE_CALLBACK_URI")
GOOGLE_CLIENT_ID = get_secret("GOOGLE_CLIENT_ID")
GOOGLE_SECRET = get_secret("GOOGLE_SECRET")

### --------

from django.shortcuts import redirect
from json import JSONDecodeError
from django.http import JsonResponse
from allauth.socialaccount.models import SocialAccount
import requests

# 로그인 페이지 연결
def google_login(request):
   scope = GOOGLE_SCOPE_USERINFO        # + "https://www.googleapis.com/auth/drive.readonly" 등 scope 설정 후 자율적으로 추가
   return redirect(f"{GOOGLE_REDIRECT}?client_id={GOOGLE_CLIENT_ID}&response_type=code&redirect_uri={GOOGLE_CALLBACK_URI}&scope={scope}")

# 인가 코드를 받아 로그인 처리
def google_callback(request):
    code = request.GET.get("code")      # Query String 으로 넘어옴
    
    token_req = requests.post(f"https://oauth2.googleapis.com/token?client_id={GOOGLE_CLIENT_ID}&client_secret={GOOGLE_SECRET}&code={code}&grant_type=authorization_code&redirect_uri={GOOGLE_CALLBACK_URI}")
    token_req_json = token_req.json()
    error = token_req_json.get("error")

    if error is not None:
        raise JSONDecodeError(error)

    google_access_token = token_req_json.get('access_token')

    email_response = requests.get(f"https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={google_access_token}")
    res_status = email_response.status_code

    if res_status != 200:
        return JsonResponse({"status": 400,"message": "Bad Request"}, status=status.HTTP_400_BAD_REQUEST)
    
    email_res_json = email_response.json()
    email = email_res_json.get('email')
	
    try:
		user = User.objects.get(email=email)

		if user is None:
    		return JsonResponse({"status": 404,"message": "User Account Not Exists"}, status=status.HTTP_404_NOT_FOUND) 
		
        # 소셜로그인 계정 유무 확인
        social_user = SocialAccount.objects.get(user=user)  
		if social_user.provider != "google":
			return JsonResponse({"status": 400,"message": "User Account Not Exists"}, status=status.HTTP_400_BAD_REQUEST) 
            
        token = RefreshToken.for_user(user)
        refresh_token = str(token)
        access_token = str(token.access_token)
		
        res = JsonResponse(
                {
                    "user": {
                        "id": user.id,
                        "email": user.email,
                    },
                    "message": "login success",
                    "token": {
                        "access_token": access_token,
                        "refresh_token": refresh_token,
                    },
                },
                status=status.HTTP_200_OK,
            )
        return res
        
    except: 
        return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  • Response가 아니라 JsonResponse로 반환하나요?
    AssertionError: .accepted_renderer not set on Response
    Response로 하면 위와 같은 에러가 발생합니다. 지금 우리는 엔드포인트에 APIView가 아니라 함수를 연결해서 실행해주고 있어요. Response - rest_framework 패키지에서 지원하는 함수 ⇒ 일반 함수에서는 사용이 불가능 ! 그래서 그냥 django 패키지에서 기본 지원하는 JsonResponse를 썼습니다 !

localhost:8000/account/google/login/ 으로 이동해서
앞에서 설정한 테스트 사용자의 계정으로 로그인하면
기존 User 데이터에 해당 구글 계정이 email로 등록된 사용자

  • 있다면 로그인 성공
  • 없다면 404 Error 발생

테스트를 원한다면 admin에서 데이터를 추가해서 테스트해주세요.

그럼 구글 로그인 완료 ~ !

django-allauth 활용 추천

django-allauth를 설치하면 아래 테이블이 추가됩니다.

account_emailaddress
account_emailconfirmation
socialaccount_socialaccount
socialaccount_socialapp
socialaccount_socialtoken

소셜로그인 제공업체 검증과 사용자 검증에 활용 추천드립니다!

profile
기록하는 감자

0개의 댓글