Django
Chapter 11-5 Django Mini Project - GitHub OAuth2 로그인 구현
Chansman
2025. 5. 16. 15:43
📌 Django Mini Project - GitHub OAuth2 로그인 구현
🔥 목표
GitHub의 OAuth2 인증 방식을 활용해 사용자가 자신의 GitHub 계정으로 로그인할 수 있도록 구현합니다.
- GitHub 개발자 설정에서 OAuth 앱 등록
- client_id와 client_secret을 이용한 인증 흐름 구현
🧭 GitHub 앱 등록 절차
- New OAuth App 클릭
- 다음 항목 입력
- Application name
- Homepage URL: 예) http://localhost:8000
- Authorization callback URL: 예) http://localhost:8000/oauth/github/callback/
- 앱 등록 후 Client ID와 Client Secret 발급
- github oauth 검색해서 ID 요청 확인 -> 토큰 발급 해서 처리
- 응답형식은 json형태를 파싱해야한다.(parse_qs 통해서)
- git hub에서 settings -> email address private 체크 해
⚙️ 설정 파일에 정보 저장 (settings.py)
# settings.py
NAVER_CLIENT_ID = SECRET['naver']['client_id']
NAVER_SECRET = SECRET['naver']['secret']
GITHUB_CLIENT_ID = SECRET['github']['client_id']
GITHUB_SECRET = SECRET['github']['secret']
👉 SECRET은 secret.json 또는 환경 변수 등을 활용하여 민감한 정보를 보호합니다.
"github": {
"client_id":"your id",
"secret": "secretkey"
}
oauth_views.py 설정 parse_qs 활용
## member/oauth_views.py
from urllib.parse import parse_qs, urlencode
import requests
from django.conf import settings
from django.contrib.auth import get_user_model, login
from django.core import signing
from django.http import Http404
from django.shortcuts import redirect, render
from django.urls import reverse
from django.views.generic import RedirectView
from member.forms import NicknameForm
User = get_user_model()
NAVER_CALLBACK_URL = '/oauth/naver/callback/'
NAVER_STATE = 'naver_login'
NAVER_LOGIN_URL = 'https://nid.naver.com/oauth2.0/authorize'
NAVER_TOKEN_URL = 'https://nid.naver.com/oauth2.0/token'
NAVER_PROFILE_URL = 'https://openapi.naver.com/v1/nid/me'
GITHUB_CALLBACK_URL = '/oauth/github/callback/'
GITHUB_STATE = 'github_login'
GITHUB_LOGIN_URL = 'https://github.com/login/oauth/authorize'
GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'
GITHUB_PROFILE_URL = 'https://api.github.com/user'
class NaverLoginRedirectView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
domain = self.request.scheme + '://' + self.request.META.get('HTTP_HOST', '')
callback_url = domain + NAVER_CALLBACK_URL
state = signing.dumps(NAVER_STATE)
params = {
'response_type': 'code',
'client_id': settings.NAVER_CLIENT_ID,
'redirect_uri': callback_url,
'state': state
}
return f'{NAVER_LOGIN_URL}?{urlencode(params)}'
def naver_callback(request):
code = request.GET.get('code')
state = request.GET.get('state')
if NAVER_STATE != signing.loads(state):
raise Http404
access_token = get_naver_access_token(code, state)
profile_response = get_naver_profile(access_token)
email = profile_response.get('email')
user = User.objects.filter(email=email).first()
if user:
if not user.is_active:
user.is_active = True
user.save()
login(request, user)
return redirect('main')
return redirect(
reverse('oauth:nickname') + f'?access_token={access_token}&oauth=naver'
)
class GithubLoginRedirectView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
domain = self.request.scheme + '://' + self.request.META.get('HTTP_HOST', '')
callback_url = domain + GITHUB_CALLBACK_URL
state = signing.dumps(GITHUB_STATE)
params = {
'response_type': 'code',
'client_id': settings.GITHUB_CLIENT_ID,
'redirect_uri': callback_url,
'state': state
}
return f'{GITHUB_LOGIN_URL}?{urlencode(params)}'
def github_callback(request):
code = request.GET.get('code')
state = request.GET.get('state')
if GITHUB_STATE != signing.loads(state):
raise Http404
access_token = get_github_access_token(code, state)
if not access_token:
raise Http404
profile_response = get_github_profile(access_token)
print('profile request', profile_response)
email = profile_response.get('email')
user = User.objects.filter(email=email).first()
if user:
if not user.is_active:
user.is_active = True
user.save()
login(request, user)
return redirect('main')
return redirect(
reverse('oauth:nickname') + f'?access_token={access_token}&oauth=github'
)
def oauth_nickname(request):
access_token = request.GET.get('access_token')
oauth = request.GET.get('oauth')
if not access_token or oauth not in ['naver', 'github']:
return redirect('login')
form = NicknameForm(request.POST or None)
if form.is_valid():
user = form.save(commit=False)
if oauth == 'naver':
profile = get_naver_profile(access_token)
else:
profile = get_github_profile(access_token)
email = profile.get('email')
if User.objects.filter(email=email).exists():
raise Http404
user.email = email
user.is_active = True
user.set_password(User.objects.make_random_password())
user.save()
login(request, user)
return redirect('main')
return render(request, 'auth/nickname.html', {'form': form})
def get_naver_access_token(code, state):
params = {
'grant_type': 'authorization_code',
'client_id': settings.NAVER_CLIENT_ID,
'client_secret': settings.NAVER_SECRET,
'code': code,
'state': state
}
response = requests.get(NAVER_TOKEN_URL, params=params)
result = response.json()
return result.get('access_token')
def get_naver_profile(access_token):
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get(NAVER_PROFILE_URL, headers=headers)
if response.status_code != 200:
raise Http404
result = response.json()
return result.get('response')
def get_github_access_token(code, state):
params = {
'grant_type': 'authorization_code',
'client_id': settings.GITHUB_CLIENT_ID,
'client_secret': settings.GITHUB_SECRET,
'code': code,
'state': state
}
response = requests.get(GITHUB_TOKEN_URL, params=params)
response_str = response.content.decode()
response_dict = parse_qs(response_str)
access_token = response_dict.get('access_token', [])[0]
return access_token
def get_github_profile(access_token):
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get(GITHUB_PROFILE_URL, headers=headers)
if response.status_code != 200:
raise Http404
result = response.json()
if not result.get('email'):
result['email'] = f'{result["login"]}@id.github.com'
return result
login.html 수정
<!-- templates/auth/login.html -->
{% extends 'base.html' %}
{% block content %}
<h1 class="title">로그인</h1>
<form method="POST">
{% csrf_token %}
{% include 'include/form.html' %}
<button class="btn btn-primary">로그인</button>
</form>
<a href="{% url 'oauth:naver_login' %}" class="btn btn-success mt-2">
네이버 로그인
</a>
<a href="{% url 'oauth:github_login' %}" class="btn btn-dark mt-2">
깃허브 로그인
</a>
{% endblock %}
member/urls.py 추가
from django.urls import path
from . import oauth_views
app_name = 'oauth'
urlpatterns = [
# naver
path('naver/login/', oauth_views.NaverLoginRedirectView.as_view(), name='naver_login'),
path('naver/callback/', oauth_views.naver_callback, name='naver_callback'),
# github
path('github/login/', oauth_views.GithubLoginRedirectView.as_view(), name='github_login'),
path('github/callback/', oauth_views.github_callback, name='github_callback'),
path('nickname/', oauth_views.oauth_nickname, name='nickname'),
]
📄 참고 문서
- GitHub OAuth 앱 권한 부여 공식 문서
- GitHub OAuth 기본 URL:
- https://github.com/login/oauth/authorize
🧱 구현해야 할 뷰 흐름 예시
- GitHubLoginRedirectView
- GitHub 로그인 URL 생성 후 리디렉션
- 파라미터: client_id, redirect_uri, scope, state
- github_callback(request)
- GitHub에서 code를 받고, access_token 요청
- 토큰으로 사용자 프로필(https://api.github.com/user) 조회
- 유저 존재 여부 확인 후 로그인 or 회원가입
- 사용자 생성 로직
- 이메일 또는 GitHub ID를 기준으로 새로운 유저 생성
- 무작위 비밀번호 + is_active=True 처리
💡 실전 팁
항목 설명
state 값 | CSRF 방지용으로 반드시 랜덤하고 검증해야 함 (signing 활용 추천) |
redirect_uri | GitHub OAuth 앱 등록 시 입력한 URL과 정확히 일치해야 함 |
access_token 요청 | https://github.com/login/oauth/access_token 로 POST 요청 필요 |
사용자 API | https://api.github.com/user 에서 로그인한 유저 정보 확인 가능 |
✅ 기대 효과
- 사용자는 GitHub 계정을 통해 간편 로그인 가능
- 추가 회원가입 없이 개발자 대상 서비스에서 효율적인 UX 제공
- OAuth2 구조를 직접 구현하며 인증 흐름을 실습할 수 있음
GitHub 로그인을 구현함으로써, 다양한 소셜 로그인 기능을 직접 확장할 수 있는 기반을 마련할 수 있습니다. Kakao, Google, Apple 등의 소셜 로그인도 같은 구조로 손쉽게 연결할 수 있어요 😊