1. Django Tutorial(Airbnb) - OAuth 프로토콜

ID짱재·2021년 8월 13일
0

Django

목록 보기
21/43
post-thumbnail

🌈 OAuth 프로토콜

🔥 Initial setting

🔥 Github Login

🔥 Kakao Login


1. Initial Setting

1) template settgins

  • FBV로 "github_login", "kakao_login"를 간단하게 만들어줍니다.
# users/views.py
def github_login(request):
    pass
def kakao_login(request):
    pass
  • 위에 만든 템플릿의 버튼을 클릭하면 호출한 FBV와 매핑시켜 줍니다.
# users/login.py
from django.urls import path
from . import views
app_name = "users"
urlpatterns = [
    path("login/", views.LoginView.as_view(), name="login"),
    path("login/github/", views.github_login, name="github-login"), # 👈 "Github Login" btn 경로
    path("login/kakao/", views.kakao_login, name="kakao-login"), # 👈 "Kakao Login" btn 경로
    path("logout/", views.log_out, name="logout"),
    path("signup/", views.SignUpView.as_view(), name="signup"),
    path("verify/<str:key>", views.complete_verification, name="complete-verification"),
]
  • partial 템플릿 디렉토리에 'social_login.html'을 별도로 분리하여 생성 후 Github 및 Kakao 버튼을 만들어 줍니다.
  • 이 템플릿은 include를 통해 'signup.html', 'login.html'에 위치시켜 사용하겠습니다. 회원가입되지 않은 계정이면, 가입 후 로그인을 진행하고 가입이 되어 있다면 바로 로그인을 해주기 위함입니다.
    • 🔍 {% include "partials/social_login.html" %}
# partial/social_login.html
<div>
    <a href="{% url 'users:gith-login' %}">
        Continue with Github
    </a>
    <a href="{% url 'users:kakao-login' %}">
        Continue with Kakao
    </a>
</div>


2. Github Login

1) OAuth App 생성

  • OAuth App은 "https://github.com/settings/developers"에서 생성할 수 있어요. OAuth App은 "Application name", "Homepage URL", "Authorization callback URL"을 입력해주면 생성됩니다.
    • "Application name" : OAuth App의 이름을 입력합니다.
    • "Homepage URL" : 홈페이지 주소("http://127.0.0.1:8000/")를 입력해줍니다.
    • "Authorization callback URL" : 사용자가 gihub에서 승인하면 redirect될 경로를 입력해줍니다.
  • 생성하면 "Client ID"와 "Client secrets"가 생성됩니다. 이는 노출시키지 않기 위해서 ".env"에 추가 후, 불러와 읽어들일 수 있도록 처리하겠습니다.
# .env
GITHUB_ID = "Client ID입력"
GITHUB_SECTET = "Client secret 입력"
  • Authorization callback URL의 경로를 urls.py에 추가하고, view와 매핑합니다.
from django.urls import path
from . import views
app_name = "users"
urlpatterns = [
    path("login/", views.LoginView.as_view(), name="login"),
    path("login/github/", views.github_login, name="github-login"),
    path("login/github/callback", views.github_callback, name="github-callback"), # 👈 callback 경로를 url에 추가합니다.
    path("login/kakao/", views.kakao_login, name="kakao-login"),
    path("logout/", views.log_out, name="logout"),
    path("signup/", views.SignUpView.as_view(), name="signup"),
    path("verify/<str:key>", views.complete_verification, name="complete-verification"),
]
  • github_login이 실행되면, GitHub identity에 request를 보내 승인을 거쳐야합니다. 이때 OAuth App에서 생성했던 ID와 승인 후 이동할 redirect_uri, scope를 파라미터로 전달해줍니다.
import os # 👈 ".env"의 값을 가져오기 위해 os를 import
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.shortcuts import render, redirect, reverse
from django.contrib.auth import authenticate, login, logout
from . import forms, models
...
...
def github_login(request):
    client_id = os.environ.get("GITHUB_ID")
    redirect_uri = "http://127.0.0.1:8000/users/login/github/callback"
    return redirect(f"https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&scope=read:user")
def github_callback(request):
    pass

2) Login Choices

  • 사용자가 Gihub나 Kakao로 가입을 요청한다면, email을 인증하지 않아도 되겠죠? 이를 체크하기 위해서 Model에 어떤 email로 회원가입을 하는지에 대한 필드를 생성해 줍니다. migration을 해주어야 합니다.
# users/model.spy
import uuid
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.core.mail import send_mail
from django.utils.html import strip_tags
from django.template.loader import render_to_string
...
...
    LOGIN_EMAIL = "email"  # 👈 choices에 들어갈 목록을 생성해 줍니다.
    LOGIN_GITHUB = "github"
    LOGIN_KAKAO = "kakao"
    LOGIN_CHOICES = (
        (LOGIN_EMAIL, "Email"),
        (LOGIN_GITHUB, "Github"),
        (LOGIN_KAKAO, "Kakao"),
    )
    avatar = models.ImageField(upload_to="avatars", blank=True)
    gender = models.CharField(choices=GENDER_CHOICES, max_length=10, blank=True)
    bio = models.TextField(blank=True)
    birthdate = models.DateField(null=True, blank=True)
    language = models.CharField(
        choices=LANGUAGE_CHOICES, max_length=2, blank=True, default=LANGUAGE_KOREAN
    )
    currency = models.CharField(
        choices=CURRENCY_CHOICES, max_length=3, blank=True, default=CURRENCY_KRW
    )
    superhost = models.BooleanField(default=False)
    email_verified = models.BooleanField(default=False)
    email_secret = models.CharField(max_length=120, default="", blank=True)
    login_method = models.CharField(
        max_length=50, choices=LOGIN_CHOICES, default=LOGIN_EMAIL
    ) # 👈 login_method 필드로 LOGIN_CHOICES를 저장하도록 해요!
...
...
  • 아래 보이는 페이지가 Github 회원가입을 눌렀을 때, Github에서 제공하는 사용자에게 보여지는 페이지입니다. 사용자가 승인을 누른다면, "http://127.0.0.1:8000/users/login/github/callback"으로 진입하기 때문에 "github_callback" 함수가 실행됩니다.

3) access token(code)

  • 사용자가 승인버튼을 눌렀을 때, 전달된 request에는 임시값(code)이 담겨있는데요, 이는 GitHub OAuth가 전달해준 임시값(code)으로 "access token"과 교환하기 위해 10분간 유효합니다.
  • 여기서 부여 받은 code를 Github에 전달하여 "access token"을 받아와야하는데요,, 이를 위해 "requests" 라이브러리를 설치해줍니다.
    • 🔍 pipenv install request
  • 공식문서를 살펴보면, "https://github.com/login/oauth/access_token" 에 POST방식으로 요청하라 되어 있습니다. 또한 이와 함께 "client_id", "client_secret", "code"를 전달해야 합니다.
  • 이를 요청하면 "access token"을 받아오는데,, "access token"을 JSON 형식으로 가져오기 위해서는 header를 추가해줘야 합니다.
  • 받아온 request.json()을 출력해보면, "access token"을 가져온 것을 확인할 수 있어요. 이제 이 "access token"를 이용해 사용자의 가입정보(Profile)를 가져올 수 있습니다.
  • "access token"를 POST로 요청했을 때, 오류가 발생할 수도 있으니 방어코드로 작성하였는데요,, "access token" 전달해서 Github API를 가져올 때는 GET 방식을 사용합니다.
  • 공식문서를 살펴보면 "https://api.github.com/user" 에 요청하라 되어있고, header정보에 "token {access_token}"를 넣어주라 되어있네요. JSON형식으로 받아오기 위해 "Accept": "application/json"를 추가합니다.
# from django.views import View
import os # 👈 .env 값을 가져오기 위해 import
import requests # 👈 github에 post 방법의 request를 보내기 위해 import
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.shortcuts import render, redirect, reverse
from django.contrib.auth import authenticate, login, logout
from . import forms, models
...
...
def github_login(request):
    client_id = os.environ.get("GITHUB_ID")
    redirect_uri = "http://127.0.0.1:8000/users/login/github/callback"
    return redirect(
        f"https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&scope=read:user"
    )
def github_callback(request):
    # print(request.GET) # 👈 10분 동안 유요한 임시 code값을 전달 받았어요:)
    client_id = os.environ.get("GITHUB_ID")
    client_secret = os.environ.get("GITHUB_SECTET")
    code = request.GET.get("code", None)
    if code is not None: # 👈 유효한 code를 받았다면(임시값이 None이 아니라면,,)
        reuslt = requests.post(
            f"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={client_secret}&code={code}",
            headers={"Accept": "application/json"}, # 👈 JSON 형식으로 요청
        )
        # print(reuslt.json()) # 👈 POST로 전달하여 받은 "access_token"값을 JSON형식으로 출력
        result_json = reuslt.json() 
        error = result_json.get("error", None)
        if error is not None: # 👈 에러값이 None이 아니라면 = 에러값을 포함하고 있다면,,
            return redirect(reverse("users:login"))
        else: # 👈 "access_token"으로 github API를 호출해 Profile정보를 가져와요:)
            access_token = result_json.get("access_token") # 👈 "access_token"을 추출합니다.
            profile_request = requests.get(  # 👈 get방식으로 API를 요청해요:)
                "https://api.github.com/user",
                headers={
                    "Authorization": f"token {access_token}",  # 👈 access_token 전달
                    "Accept": "application/json",  # 👈 JSON 형식으로 요청
                },
            )
            print(profile_request.json())
    else:
        return redirect(reverse("core:home"))

4) Getting Github Profile

  • "profile_request.json()" 값으로 Github Profile 정보에 접근하여 name, email, bio 정보를 가져와 계정을 생성해 줍니다.
  • 단, 계정 생성하기 전에 "profile_request.json()"로 가져온 email값이 이미 DB에 있는지 확인해야해요. 만일 이미 DB에 존재하는 email이면서 "login_method"의 값이 "github"로 되어있다면 login을 시켜주고, "github"가 아니라면 에러를 발생시켜줍니다. 또한 DB에 email이 존재하지 않는다면 계정을 생성합니다. 이를 통해 회원가입과 로그인의 로직을 통합하여 처리할 수 있어요.
  • 또한 계정을 DB에 새로 생성해줄 경우에는 login_method를 Github로 저장해주고,, 또한 비밀번호는 "set_unusable_password()"를 통해 생성해줍니다. "set_unusable_password()"는 자동으로 저장해주지않기 때문에 save()를 반드시해주어야 합니다:)
# from django.views import View
import os
import requests
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.shortcuts import render, redirect, reverse
from django.contrib.auth import authenticate, login, logout
from . import forms, models
...
...
def github_login(request):
    client_id = os.environ.get("GITHUB_ID")
    redirect_uri = "http://127.0.0.1:8000/users/login/github/callback"
    return redirect(
        f"https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&scope=read:user"
    ) # 👈 사용자가 승인을 누르면, redirect_uri 경로로 redirect 됩니다.
class GithubException(Exception):
    pass
def github_callback(request):
    try:
        client_id = os.environ.get("GITHUB_ID")
        client_secret = os.environ.get("GITHUB_SECTET")
        code = request.GET.get("code", None)
        if code is not None: # 👈 임시 code가 None이 아니라면,,
            reuslt = requests.post(
                f"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={client_secret}&code={code}",
                headers={"Accept": "application/json"},
            ) # 👈 JSON형식으로 "access_token"을 POST 방식으로 요청합니다:)
            result_json = reuslt.json()
            error = result_json.get("error", None)
            if error is not None: # 👈 에러코드를 전달받았다면 에러를 발생시킵니다:)
                raise GithubException()
            else: # 👈 "access_token"을 받았다면,,
                access_token = result_json.get("access_token")
                profile_request = requests.get(
                    "https://api.github.com/user",
                    headers={
                        "Authorization": f"token {access_token}",
                        "Accept": "application/json",
                    },
                ) # 👈 github profile을 JSON 형식으로 요청합니다:)
                profile_json = profile_request.json()
                username = profile_json.get("login", None) # 👈 login값에는 사용자 이름이 담겨있어요.
                if username is not None: # 👈 사용자 이름이 비어있지 않다면,,
                    name = profile_json.get("name")
                    email = profile_json.get("email")
                    bio = profile_json.get("bio")
                    try: # 👈 email이 이미 DB에 존재하는지 확인합니다.
                        user = models.User.objects.get(email=email)
                        if user.login_method != models.User.LOGIN_GITHUB:
                            raise GithubException() 
                    except models.User.DoesNotExist: # 👈 email이 이미 DB에 존재하지 않다면,,
                        user = models.User.objects.create(
                            email=email,
                            first_name=name,
                            username=email,
                            bio=bio,
                            login_method=models.User.LOGIN_GITHUB,
                            email_verified=True,
                        )
                        user.set_unusable_password() # 👈 암호 설정
                        user.save()
                    login(request, user)
                    return redirect(reverse("core:home"))
                else:
                    raise GithubException()
        else:
            raise GithubException()
    except GithubException:
        return redirect(reverse("users:login"))

3. Kakao Login

1) OAuth App 생성

  • OAuth App 생성은 "https://developers.kakao.com/console/app" 에서 생성할 수 있어요. 앱이름과 회사를 입력하면 생성됩니다. 여기서 REST API를 "app_key"로 사용하면 됩니다.
  • 단, 카카오는 생성된 "OAuth App"에 카카오 로그인 활성화 설정("ON")을 해주어야 합니다. 이와 함께 Redirect URI를 설정해줍니다.
  • 이와 함께 [동의항목]으로 이동해, 사용자가 회원가입 시, 계정 생성을 위해 카카오로부터 받아올 정도를 동의해 줍니다. 이 동의한 내용을 바탕으로 사용자에게 승인 팝업이 제공되며, 동의된 내용만 서버로 가져올 수 있어요! 샘플을 확인하고 싶다면 상단에 "동의화면 미리보기"를 클릭해보세요!
  • "app_key", "redirect_uri"를 가지고, redirect 시키면, 아래와 같이 승인 페이지로 요청됩니다.
# from django.views import View
import os
import requests
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.shortcuts import render, redirect, reverse
from django.contrib.auth import authenticate, login, logout
from . import forms, models
...
...
def kakao_login(request):
    client_id = os.environ.get("KAKAO_ID")
    redirect_uri = "http://127.0.0.1:8000/users/login/kakao/callback"
    return redirect(
        f"https://kauth.kakao.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code"
    )
def kakao_callback(request):
    print(request)

2) Getting KaKao Profile

  • 사진을 저장하기 위해서는 "ContentFile"를 가져와야해요,, save 매서드 안에 사진 이름을 지정하고, "ContentFile"에 사진을 코드형태로 전달하면 됩니다. 이미지에 .content를 붙여주면 0과 1로 변환되요!
    • 🔎 user.avatar.save(f"{nickname}-avatar", ContentFile(photo_request.content))
# from django.views import View
import os
import requests
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.shortcuts import render, redirect, reverse
from django.contrib.auth import authenticate, login, logout
from django.core.files.base import ContentFile # 👈 "ContentFile" import
from . import forms, models
...
...
def kakao_login(request):
    client_id = os.environ.get("KAKAO_ID")
    redirect_uri = "http://127.0.0.1:8000/users/login/kakao/callback"
    return redirect(
        f"https://kauth.kakao.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code"
    )
class KakaoException(Exception): # 👈 에러처리를 위해 class 생성
    pass
def kakao_callback(request):
    try:
        code = request.GET.get("code") # 👈 임시 코드를 받아옵니다.
        client_id = os.environ.get("KAKAO_ID")
        redirect_uri = "http://127.0.0.1:8000/users/login/kakao/callback"
        token_request = requests.get( # 👈 code로 access_token을 JSON형태로 요청
            f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={client_id}&redirect_uri={redirect_uri}&code={code}" 
        )
        # print(token_request.json())
        token_json = token_request.json()
        error = token_json.get("error", None)
        if error is not None: # 👈 token 값이 없다면,
            raise KakaoException()
        access_token = token_json.get("access_token") # 👈 access_token 추출
        profile_request = requests.get
            "https://kapi.kakao.com//v2/user/me",
            headers={"Authorization": f"Bearer {access_token}"},
        ) # 👈 "access_token"으로 API(profile) 요청
        profile_json = profile_request.json()
        email = profile_json.get("kakao_account").get("email") # 👈 email 값 
        if email is None: # 👈 email이 없다면 에러 발생
            raise KakaoException()
        properties = profile_json.get("kakao_account").get("profile")
        nickname = properties.get("nickname") # 👈 이름값
        profile_image = properties.get("profile_image_url") # 👈 사진정보
        # print(nickname, profile_image)
        try: # 👈  DB에 email이 존재하는지 확인
            user = models.User.objects.get(email=email)
            if user.login_method != models.User.LOGIN_KAKAO:
                raise KakaoException() 
        except models.User.DoesNotExist: # 👈 DB에 없다면 계정 생성
            user = models.User.objects.create(
                email=email,
                first_name=nickname,
                username=email,
                login_method=models.User.LOGIN_KAKAO,
                email_verified=True,
            )
            user.set_unusable_password()
            user.save()
            if profile_image is not None: # 👈 Kakao profile에 image가 있다면,
                photo_request = requests.get(profile_image)
                user.avatar.save(
                    f"{nickname}-avatar", ContentFile(photo_request.content)
                ) # 👈 사진을 파일로 저장
        login(request, user)
        return redirect(reverse("core:home"))
    except KakaoException:
        return redirect(reverse("users:login"))
profile
Keep Going, Keep Coding!

0개의 댓글