Django 인증 시스템
Django는 강력한 인증 시스템을 내장하고 있습니다. FastAPI에서는 JWT 토큰을 직접 구현했지만, Django는 세션 기반 인증을 기본으로 제공하며 사용자 관리에 필요한 모든 기능을 갖추고 있습니다.
1. Django Auth vs FastAPI JWT
인증 방식 비교
FastAPI (JWT):
# 토큰 기반 인증
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
Django (세션):
# 세션 기반 인증
def login_view(request):
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return redirect('home')
특징 비교
| 특징 | Django (세션) | FastAPI (JWT) |
|---|---|---|
| 상태 | Stateful | Stateless |
| 저장소 | 서버 세션 | 클라이언트 토큰 |
| 확장성 | 세션 공유 필요 | 서버 독립적 |
| 보안 | CSRF 보호 필요 | 토큰 탈취 위험 |
| 만료 처리 | 자동 | 수동 구현 |
2. Django User 모델
기본 User 모델
from django.contrib.auth.models import User
# User 모델의 주요 필드
# - username (required)
# - password (required)
# - email
# - first_name
# - last_name
# - is_active
# - is_staff
# - is_superuser
# - date_joined
# - last_login
# 사용자 생성
user = User.objects.create_user(
username='john',
email='john@example.com',
password='secure_password'
)
# 슈퍼유저 생성
superuser = User.objects.create_superuser(
username='admin',
email='admin@example.com',
password='admin_password'
)
커스텀 User 모델
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
# 추가 필드
phone = models.CharField(max_length=20, blank=True)
birth_date = models.DateField(null=True, blank=True)
profile_image = models.ImageField(upload_to='profiles/', blank=True)
bio = models.TextField(blank=True)
# 이메일을 로그인 ID로 사용
email = models.EmailField(unique=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
# settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'
3. 인증 뷰 구현
로그인
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.forms import AuthenticationForm
from django.shortcuts import render, redirect
from django.contrib import messages
def login_view(request):
if request.user.is_authenticated:
return redirect('home')
if request.method == 'POST':
form = AuthenticationForm(request, data=request.POST)
if form.is_valid():
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
user = authenticate(username=username, password=password)
if user is not None:
login(request, user)
messages.success(request, f'{username}님 환영합니다!')
# next 파라미터 처리
next_url = request.GET.get('next', 'home')
return redirect(next_url)
else:
form = AuthenticationForm()
return render(request, 'accounts/login.html', {'form': form})
로그아웃
from django.contrib.auth.decorators import login_required
@login_required
def logout_view(request):
if request.method == 'POST':
logout(request)
messages.info(request, '로그아웃되었습니다.')
return redirect('home')
return render(request, 'accounts/logout_confirm.html')
회원가입
from django.contrib.auth.forms import UserCreationForm
class SignUpForm(UserCreationForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data['email']
if commit:
user.save()
return user
def signup_view(request):
if request.method == 'POST':
form = SignUpForm(request.POST)
if form.is_valid():
user = form.save()
# 자동 로그인
login(request, user)
messages.success(request, '회원가입이 완료되었습니다!')
return redirect('home')
else:
form = SignUpForm()
return render(request, 'accounts/signup.html', {'form': form})
4. 로그인 템플릿
<!-- templates/accounts/login.html -->
{% extends 'base.html' %}
{% block content %}
<div class="login-container">
<h2>로그인</h2>
<form method="post">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
<div class="form-group">
{{ form.username.label_tag }}
{{ form.username }}
{% if form.username.errors %}
<div class="text-danger">{{ form.username.errors }}</div>
{% endif %}
</div>
<div class="form-group">
{{ form.password.label_tag }}
{{ form.password }}
{% if form.password.errors %}
<div class="text-danger">{{ form.password.errors }}</div>
{% endif %}
</div>
<button type="submit" class="btn btn-primary">로그인</button>
<a href="{% url 'signup' %}" class="btn btn-link">회원가입</a>
<a href="{% url 'password_reset' %}" class="btn btn-link">비밀번호 찾기</a>
</form>
<!-- 소셜 로그인 -->
<div class="social-login">
<h3>소셜 계정으로 로그인</h3>
<a href="{% url 'social:begin' 'google-oauth2' %}" class="btn btn-google">
Google로 로그인
</a>
<a href="{% url 'social:begin' 'github' %}" class="btn btn-github">
GitHub로 로그인
</a>
</div>
</div>
{% endblock %}
5. 권한과 그룹
권한 확인
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
# 함수 기반 뷰
@login_required
@permission_required('blog.add_post', raise_exception=True)
def create_post(request):
# 로그인 + 포스트 생성 권한 필요
pass
# 클래스 기반 뷰
class PostCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
permission_required = 'blog.add_post'
model = Post
그룹 관리
from django.contrib.auth.models import Group, Permission
# 그룹 생성
editors = Group.objects.create(name='Editors')
# 권한 추가
post_permissions = Permission.objects.filter(
content_type__app_label='blog',
codename__in=['add_post', 'change_post', 'delete_post']
)
editors.permissions.set(post_permissions)
# 사용자를 그룹에 추가
user.groups.add(editors)
# 템플릿에서 권한 체크
"""
{% if perms.blog.add_post %}
<a href="{% url 'blog:post_create' %}">글쓰기</a>
{% endif %}
"""
6. 비밀번호 관리
비밀번호 재설정
# urls.py
from django.contrib.auth import views as auth_views
urlpatterns = [
# 비밀번호 재설정 요청
path('password-reset/',
auth_views.PasswordResetView.as_view(
template_name='accounts/password_reset.html',
email_template_name='accounts/password_reset_email.html',
success_url='/password-reset/done/'
),
name='password_reset'),
# 이메일 전송 완료
path('password-reset/done/',
auth_views.PasswordResetDoneView.as_view(
template_name='accounts/password_reset_done.html'
),
name='password_reset_done'),
# 비밀번호 재설정 폼
path('password-reset-confirm/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(
template_name='accounts/password_reset_confirm.html',
success_url='/password-reset-complete/'
),
name='password_reset_confirm'),
# 재설정 완료
path('password-reset-complete/',
auth_views.PasswordResetCompleteView.as_view(
template_name='accounts/password_reset_complete.html'
),
name='password_reset_complete'),
]
비밀번호 변경
@login_required
def password_change(request):
if request.method == 'POST':
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
user = form.save()
# 세션 유지
update_session_auth_hash(request, user)
messages.success(request, '비밀번호가 변경되었습니다.')
return redirect('profile')
else:
form = PasswordChangeForm(request.user)
return render(request, 'accounts/password_change.html', {'form': form})
7. 사용자 프로필
# models.py
from django.db.models.signals import post_save
from django.dispatch import receiver
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
location = models.CharField(max_length=100, blank=True)
birth_date = models.DateField(null=True, blank=True)
avatar = models.ImageField(upload_to='avatars/', default='avatars/default.png')
def __str__(self):
return f'{self.user.username} Profile'
# 자동 프로필 생성
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.userprofile.save()
# views.py
@login_required
def profile_edit(request):
profile = request.user.userprofile
if request.method == 'POST':
form = UserProfileForm(request.POST, request.FILES, instance=profile)
if form.is_valid():
form.save()
messages.success(request, '프로필이 업데이트되었습니다.')
return redirect('profile')
else:
form = UserProfileForm(instance=profile)
return render(request, 'accounts/profile_edit.html', {'form': form})
8. 소셜 인증 (django-allauth)
# settings.py
INSTALLED_APPS = [
# ...
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',
'allauth.socialaccount.providers.github',
]
SITE_ID = 1
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
# allauth 설정
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
9. 커스텀 인증 백엔드
# backends.py
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.models import User
class EmailBackend(BaseBackend):
"""이메일로 로그인"""
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(email=username)
if user.check_password(password):
return user
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
# settings.py
AUTHENTICATION_BACKENDS = [
'accounts.backends.EmailBackend',
'django.contrib.auth.backends.ModelBackend',
]
10. 보안 설정
세션 보안
# settings.py
SESSION_COOKIE_SECURE = True # HTTPS만
SESSION_COOKIE_HTTPONLY = True # JavaScript 접근 방지
SESSION_COOKIE_SAMESITE = 'Strict'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
# 로그인 시도 제한
AXES_FAILURE_LIMIT = 5 # django-axes 패키지
미들웨어로 접근 제어
# middleware.py
class LoginRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.exempt_urls = ['/login/', '/signup/', '/api/']
def __call__(self, request):
if not request.user.is_authenticated:
path = request.path_info
if not any(path.startswith(url) for url in self.exempt_urls):
return redirect(f'/login/?next={path}')
response = self.get_response(request)
return response
실습: 사용자 대시보드
# views.py
@login_required
def dashboard(request):
user_posts = Post.objects.filter(author=request.user)
user_comments = Comment.objects.filter(author=request.user)
# 통계
stats = {
'total_posts': user_posts.count(),
'published_posts': user_posts.filter(status='published').count(),
'total_comments': user_comments.count(),
'total_views': user_posts.aggregate(Sum('view_count'))['view_count__sum'] or 0,
}
# 최근 활동
recent_posts = user_posts.order_by('-created_at')[:5]
recent_comments = user_comments.order_by('-created_at')[:5]
context = {
'stats': stats,
'recent_posts': recent_posts,
'recent_comments': recent_comments,
}
return render(request, 'accounts/dashboard.html', context)
정리
Django 인증 시스템의 특징: - 완성된 시스템: User 모델, 인증 뷰, 폼 모두 제공 - 세션 기반: 서버 사이드 상태 관리 - 보안 기능: CSRF 보호, 비밀번호 해싱 등 내장 - 확장 가능: 커스텀 User 모델, 인증 백엔드 - 권한 시스템: 세밀한 권한 관리 가능
FastAPI와 비교: - Django는 세션 기반, FastAPI는 주로 토큰 기반 - Django는 인증 시스템이 내장, FastAPI는 직접 구현 - Django는 템플릿과 통합, FastAPI는 API 중심 - 두 프레임워크 모두 유연한 커스터마이징 가능
다음 장에서는 클래스 기반 뷰(CBV)를 알아보겠습니다!