TIL(22/11/21)

김규현·2022년 11월 24일
0

📝 오늘 배운 것

📌초기환경 세팅

JWT기반 인증을 해야하기 때문에 simplejwt 패키지 설치가 필요하며, 장고의 유저 기능들을 구현하기 위한 django_allauth 패키지와 소셜 로그인을 위한 dj-rest-auth 패키지를 설치한다.

pip install djangorestframework
pip install djangorestframework-simplejwt
pip install django-allauth
pip install dj-rest-auth

외부 노출을 방지하기 위해 django의 secret_key를 분리해준다.

secret.json

{
    "SOCIAL_AUTH_GOOGLE_CLIENT_ID" : "google_client_id",
    "SOCIAL_AUTH_GOOGLE_SECRET" : "google_secret_key",
    "STATE" : "random_string",
    "DATABASES" : {
        "default": {
            "ENGINE": "django.db.backends.sqlite3",
            "NAME": "db_name",
            "USER": "db_user",
            "PASSWORD": "db_password",
            "HOST": "localhost",
            "PORT" : "3306",
         }}
}

settings.py

# 별도 파일로 관리중인 SECRET_KEY를 django에 연결 
from pathlib import Path
import os
import json
import sys

BASE_DIR = Path(__file__).resolve().parent.parent
ROOT_DIR = os.path.dirname(BASE_DIR)
SECRET_BASE_FILE = os.path.join(BASE_DIR, 'secrets.json')
secrets = json.loads(open(SECRET_BASE_FILE).read())
for key, value in secrets.items():
    setattr(sys.modules[__name__], key, value)
    
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
     
    'django.contrib.sites', 
    
    # my app
    'users',
    
    #django-rest-auth
    'rest_framework',
    'dj_rest_auth',
    'dj_rest_auth.registration',
	
    #django-allauth
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
]

# Custom UserModel
AUTH_USER_MODEL = 'users.User' 
SITE_ID = 1

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.AllowAny',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
    ),
}

# 커스텀한 유저 모델의 USERNAME_FIELD를 username -> email로 변경했으므로 ACCOUNT설정
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_VERIFICATION = 'none'

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# JWT 환경 설정
from datetime import timedelta

REST_USE_JWT = True
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(hours=2),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': True,
}

User 모델이 있는 users 앱에 settings에서 추가한 라이브러리들을 매핑한다.
urls.py

urlpatterns = [
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
    path('users/', include('dj_rest_auth.urls')),
    path('users/', include('allauth.urls')),
]

setiings 설정이 끝나고 migration 후 admin 페이지의 site를 localhost:8000으로 추가

Social applications에서 소셜로그인을 제공할 플랫폼을 선택하고, 이름과 client_id, secret_key를 입력 후 이전에 추가한 localhost의 site를 선택하여 Social application을 추가해준다.

client_id, secret_key는 Google API 에서 앱 등록 후 사용자 인증 정보에서 Redirection URI를 설정하여 발급받을 수 있다.

📌users.urls.py

urlpatterns = [
    path('google/login', views.google_login, name='google_login'),
    path('google/callback/', views.google_callback, name='google_callback'),  
    path('google/login/finish/', views.GoogleLogin.as_view(), name='google_login_todjango'),
]

📌users.views.py

google_login 실행 후 로그인 성공 시, Callback 함수로 Code 값 전달
받은 Code와 Google에 Access Token 요청
Access Token으로 Email 값을 Google에게 요청
전달받은 Email, Access Token, Code를 바탕으로 회원가입/로그인 진행

from users.models import User
from django.conf import settings
from django.http import JsonResponse
from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.models import SocialAccount
from allauth.socialaccount.providers.google import views as google_view
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from rest_framework import status
from json.decoder import JSONDecodeError
import requests

state = getattr(settings, 'STATE')
BASE_URL = 'http://localhost:8000/'
GOOGLE_CALLBACK_URI = BASE_URL + 'users/google/callback/'

def google_login(request):
    """
    Code Request
    """
    scope = "https://www.googleapis.com/auth/userinfo.email"
    client_id = getattr(settings, "SOCIAL_AUTH_GOOGLE_CLIENT_ID")
    return redirect(f"https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}&response_type=code&redirect_uri={GOOGLE_CALLBACK_URI}&scope={scope}")

def google_callback(request):
    client_id = getattr(settings, "SOCIAL_AUTH_GOOGLE_CLIENT_ID")
    client_secret = getattr(settings, "SOCIAL_AUTH_GOOGLE_SECRET")
    code = request.GET.get('code')
    
    """
    Access Token Request
    """
    token_req = requests.post(
        f"https://oauth2.googleapis.com/token?client_id={client_id}&client_secret={client_secret}&code={code}&grant_type=authorization_code&redirect_uri={GOOGLE_CALLBACK_URI}&state={state}")
    token_req_json = token_req.json()
    error = token_req_json.get("error")
    if error is not None:
        raise JSONDecodeError(error)
    access_token = token_req_json.get('access_token')
    
    """
    Email Request
    """
    email_req = requests.get(
        f"https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={access_token}")
    email_req_status = email_req.status_code
    if email_req_status != 200:
        return JsonResponse({'err_msg': 'failed to get email'}, status=status.HTTP_400_BAD_REQUEST)
    email_req_json = email_req.json()
    email = email_req_json.get('email')
    
    """
    Signup or Signin Request
    """
    try:
        user = User.objects.get(email=email)
        # 기존에 가입된 유저의 Provider가 google이 아니면 에러 발생, 맞으면 로그인
        # 다른 SNS로 가입된 유저
        social_user = SocialAccount.objects.get(user=user)
        if social_user is None:
            return JsonResponse({'err_msg': 'email exists but not social user'}, status=status.HTTP_400_BAD_REQUEST)
        if social_user.provider != 'google':
            return JsonResponse({'err_msg': 'no matching social type'}, status=status.HTTP_400_BAD_REQUEST)
        # 기존에 Google로 가입된 유저
        data = {'access_token': access_token, 'code': code}
        accept = requests.post(
            f"{BASE_URL}users/google/login/finish/", data=data)
        accept_status = accept.status_code
        if accept_status != 200:
            return JsonResponse({'err_msg': 'failed to signin'}, status=accept_status)
        accept_json = accept.json()
        accept_json.pop('user', None)
        return JsonResponse(accept_json)
    
    except User.DoesNotExist:
        # 기존에 가입된 유저가 없으면 새로 가입
        data = {'access_token': access_token, 'code': code}
        accept = requests.post(
            f"{BASE_URL}users/google/login/finish/", data=data)
        accept_status = accept.status_code
        if accept_status != 200:
            return JsonResponse({'err_msg': 'failed to signup'}, status=accept_status)
        accept_json = accept.json()
        accept_json.pop('user', None)
        return JsonResponse(accept_json)

class GoogleLogin(SocialLoginView):
    adapter_class = google_view.GoogleOAuth2Adapter
    callback_url = GOOGLE_CALLBACK_URI
    client_class = OAuth2Client

위와 같이 코드를 작성했을 때 소셜 로그인으로 access token과 refresh token을 발급받을 수 있었고,
jwt에서 토큰 정보를 확인해보면 DB에 등록된 사용자임을 알 수 있었다.

하지만 소셜 로그인 과정이 종료된 후 access token, refresh token이 보여지는 페이지로 redirect 되는데 로그인 이후 token이 보여지지 않고 내 프로젝트의 main 페이지로 이동시켜주어야 한다.

소셜 로그인으로 사용자 인증 처리 후 어떻게 메인페이지로 이동시킬지 찾아봐야 할 것 같다.

profile
웹개발 회고록

0개의 댓글