🌈 OAuth 프로토콜
🔥 Initial setting
🔥 Github Login
🔥 Kakao Login
1. Initial Setting
1) template settgins
- FBV로 "github_login", "kakao_login"를 간단하게 만들어줍니다.
def github_login(request):
pass
def kakao_login(request):
pass
- 위에 만든 템플릿의 버튼을 클릭하면 호출한 FBV와 매핑시켜 줍니다.
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/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"),
]
- partial 템플릿 디렉토리에 'social_login.html'을 별도로 분리하여 생성 후 Github 및 Kakao 버튼을 만들어 줍니다.
- 이 템플릿은 include를 통해 'signup.html', 'login.html'에 위치시켜 사용하겠습니다. 회원가입되지 않은 계정이면, 가입 후 로그인을 진행하고 가입이 되어 있다면 바로 로그인을 해주기 위함입니다.
- 🔍 {% include "partials/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"에 추가 후, 불러와 읽어들일 수 있도록 처리하겠습니다.
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"),
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
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을 해주어야 합니다.
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"
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
)
...
...
3) access token(code)
- 사용자가 승인버튼을 눌렀을 때, 전달된 request에는 임시값(code)이 담겨있는데요, 이는 GitHub OAuth가 전달해준 임시값(code)으로 "access token"과 교환하기 위해 10분간 유효합니다.
- 여기서 부여 받은 code를 Github에 전달하여 "access token"을 받아와야하는데요,, 이를 위해 "requests" 라이브러리를 설치해줍니다.
- 공식문서를 살펴보면, "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"를 추가합니다.
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"
)
def github_callback(request):
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:
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"},
)
result_json = reuslt.json()
error = result_json.get("error", None)
if error is not None:
return redirect(reverse("users:login"))
else:
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",
},
)
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()를 반드시해주어야 합니다:)
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"
)
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:
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"},
)
result_json = reuslt.json()
error = result_json.get("error", None)
if error is not None:
raise GithubException()
else:
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",
},
)
profile_json = profile_request.json()
username = profile_json.get("login", None)
if username is not None:
name = profile_json.get("name")
email = profile_json.get("email")
bio = profile_json.get("bio")
try:
user = models.User.objects.get(email=email)
if user.login_method != models.User.LOGIN_GITHUB:
raise GithubException()
except models.User.DoesNotExist:
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 시키면, 아래와 같이 승인 페이지로 요청됩니다.
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))
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
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):
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(
f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={client_id}&redirect_uri={redirect_uri}&code={code}"
)
token_json = token_request.json()
error = token_json.get("error", None)
if error is not None:
raise KakaoException()
access_token = token_json.get("access_token")
profile_request = requests.get
"https://kapi.kakao.com//v2/user/me",
headers={"Authorization": f"Bearer {access_token}"},
)
profile_json = profile_request.json()
email = profile_json.get("kakao_account").get("email")
if email is None:
raise KakaoException()
properties = profile_json.get("kakao_account").get("profile")
nickname = properties.get("nickname")
profile_image = properties.get("profile_image_url")
try:
user = models.User.objects.get(email=email)
if user.login_method != models.User.LOGIN_KAKAO:
raise KakaoException()
except models.User.DoesNotExist:
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:
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"))