인증은 "누구인가"를 확인하는 일이다
게시판을 공개 목록만 보여 주는 수준으로 만든다면 로그인 기능이 없어도 동작한다. 하지만 글 작성, 내 글 수정, 관리자 기능, 비공개 페이지처럼 사용자를 구분해야 하는 순간이 오면 인증과 권한이 필요하다.
인증은 사용자가 누구인지 확인하는 과정이다. 보통 아이디와 비밀번호로 로그인하는 일을 말한다. 권한은 그 사용자가 어떤 행동을 할 수 있는지 판단하는 일이다. 예를 들어 로그인한 사용자는 글을 쓸 수 있지만, 다른 사람이 쓴 글은 수정하지 못하게 막는 것이 권한 관리다.
Django는 인증과 권한에 필요한 많은 기능을 기본으로 제공한다. 처음 프로젝트를 만들면 보통 INSTALLED_APPS에 다음 앱들이 이미 들어 있다.
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]여기서 django.contrib.auth가 사용자, 그룹, 권한, 로그인 검증을 담당한다. django.contrib.sessions는 로그인 상태를 서버 세션에 저장한다. 사용자가 로그인하면 Django는 브라우저 쿠키에 세션 ID를 보내고, 다음 요청부터 그 세션 ID를 기준으로 "이 요청은 어떤 사용자에게서 왔는가"를 확인한다.
일반적인 Django 프로젝트의 기본 설정에는 세션 미들웨어와 인증 미들웨어도 함께 들어 있다. 그래서 View 안에서는 다음처럼 현재 사용자를 확인할 수 있다.
def sample_view(request):
user = request.user기본 사용자 모델을 쓰는 프로젝트라면, 로그인한 사용자의 request.user는 User 객체다. 로그인하지 않았다면 AnonymousUser 객체다. 둘 다 is_authenticated 속성을 가지고 있기 때문에 다음처럼 사용할 수 있다.
if request.user.is_authenticated:
# 로그인한 사용자
else:
# 로그인하지 않은 사용자이 구조 덕분에 매번 세션을 직접 읽거나 쿠키를 직접 해석할 필요가 없다.
AI 생성 이미지
User 모델 이해하기
Django의 기본 사용자 모델은 User다. 이 모델에는 로그인과 권한 관리에 필요한 필드가 이미 포함되어 있다.
username: 로그인 이름password: 해시 처리되어 저장되는 비밀번호 정보email: 이메일first_name,last_name: 이름 정보is_active: 활성 사용자 여부is_staff: 관리자 사이트 접근 가능 여부is_superuser: 모든 권한을 가진 사용자 여부
중요한 점은 비밀번호가 평문으로 저장되지 않는다는 것이다. Django는 비밀번호를 해시 값으로 저장한다. 해시는 원래 비밀번호를 그대로 보관하지 않고 검증 가능한 형태로 바꾸는 방식이다. 사용자가 로그인하면 입력한 비밀번호를 같은 방식으로 처리해 저장된 값과 비교한다.
기본 User 모델만으로도 일반적인 게시판 로그인 기능은 충분히 만들 수 있다. 나중에 서비스에서 닉네임, 프로필 이미지, 생년월일 같은 추가 정보가 필요하면 별도 프로필 모델을 만들거나 사용자 모델을 커스터마이징할 수 있다. 다만 이 장에서는 Django 기본 사용자 모델을 사용해 흐름을 익힌다.
회원가입 만들기
Django는 로그인, 로그아웃, 비밀번호 변경 같은 View를 기본 제공하지만, 회원가입 화면은 프로젝트 성격에 따라 달라지는 부분이 많아 직접 만드는 경우가 많다.
계정 기능을 분리하기 위해 accounts 앱을 만든다고 가정하자.
python manage.py startapp accounts그리고 settings.py에 등록한다.
INSTALLED_APPS = [
# ...
"accounts",
]회원가입 폼은 Django가 제공하는 UserCreationForm을 사용할 수 있다.
# accounts/views.py
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import login
from django.shortcuts import render, redirect
def signup(request):
if request.method == "POST":
form = UserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect("boards:post_list")
else:
form = UserCreationForm()
return render(request, "accounts/signup.html", {"form": form})UserCreationForm은 사용자 이름과 비밀번호 입력, 비밀번호 확인 검증을 처리한다. form.save()는 새 User 객체를 만든다. 그다음 login(request, user)를 호출하면 가입 직후 바로 로그인 상태가 된다.
URL도 연결한다. 이 장에서는 accounts 앱 안에 계정 관련 URL을 모으고, URL 이름에는 accounts: 네임스페이스를 붙여서 사용하겠다.
# accounts/urls.py
from django.urls import path
from . import views
app_name = "accounts"
urlpatterns = [
path("signup/", views.signup, name="signup"),
]프로젝트 URL에서 accounts 앱 URL을 포함한다.
# config/urls.py
from django.urls import path, include
urlpatterns = [
path("accounts/", include("accounts.urls")),
]템플릿은 이전 장에서 만든 폼 처리 방식과 같다.
<!-- accounts/templates/accounts/signup.html -->
{% extends "base.html" %}
{% block content %}
<h2>회원가입</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">가입하기</button>
</form>
{% endblock %}회원가입도 사용자 입력을 서버에 저장하는 작업이므로 반드시 POST와 {% csrf_token %}을 사용한다.
기본 인증 View 연결하기
로그인, 로그아웃, 비밀번호 변경은 Django가 제공하는 클래스 기반 View를 그대로 사용할 수 있다. 가장 간단한 방법은 django.contrib.auth.urls를 accounts/urls.py 안에 포함하는 것이다.
# accounts/urls.py
from django.urls import path, include
from . import views
app_name = "accounts"
urlpatterns = [
path("signup/", views.signup, name="signup"),
path("", include("django.contrib.auth.urls")),
]프로젝트 URL 설정은 그대로 둔다.
# config/urls.py
from django.urls import path, include
urlpatterns = [
path("accounts/", include("accounts.urls")),
]이렇게 하면 대표적으로 다음 URL들이 추가된다.
accounts/login/accounts/logout/accounts/password_change/accounts/password_change/done/accounts/password_reset/accounts/password_reset/done/accounts/reset/<uidb64>/<token>/accounts/reset/done/
그리고 accounts 앱 URL 안에 포함했으므로 템플릿에서는 accounts:login, accounts:logout처럼 이름을 사용할 수 있다.
로그인 화면은 기본적으로 registration/login.html 템플릿을 찾는다. 따라서 다음 위치에 템플릿을 만든다.
<!-- templates/registration/login.html -->
{% extends "base.html" %}
{% block content %}
<h2>로그인</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">로그인</button>
</form>
{% endblock %}최근 Django에서는 로그아웃을 POST 요청으로 처리하는 흐름을 사용한다. 링크를 눌러 바로 로그아웃시키기보다 버튼이 있는 폼을 사용하는 편이 안전하고, 최신 버전의 동작 방식과도 잘 맞다.
{% if user.is_authenticated %}
<form action="{% url 'accounts:logout' %}" method="post">
{% csrf_token %}
<button type="submit">로그아웃</button>
</form>
{% else %}
<a href="{% url 'accounts:login' %}">로그인</a>
{% endif %}여기서 주의할 점이 하나 있다. django.contrib.auth.urls를 프로젝트 URL에 직접 다음처럼 포함하면 URL 이름은 login, logout처럼 전역 이름으로 등록된다.
path("accounts/", include("django.contrib.auth.urls"))반대로 이 장의 예제처럼 accounts/urls.py 안에 포함하고 app_name = "accounts"를 두면 accounts:login, accounts:logout처럼 네임스페이스가 붙은 이름을 사용한다. 두 방식을 섞으면 템플릿에서 URL을 찾지 못해 오류가 날 수 있으니, 한 가지 방식으로 맞추는 것이 좋다.
템플릿 위치와 URL 이름을 더 명확하게 정하고 싶다면 LoginView, LogoutView, PasswordChangeView를 직접 연결해도 된다.
# accounts/urls.py
from django.urls import path, reverse_lazy
from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView
from . import views
app_name = "accounts"
urlpatterns = [
path("signup/", views.signup, name="signup"),
path("login/", LoginView.as_view(template_name="accounts/login.html"), name="login"),
path("logout/", LogoutView.as_view(), name="logout"),
path(
"password/change/",
PasswordChangeView.as_view(
template_name="accounts/password_change.html",
success_url=reverse_lazy("boards:post_list"),
),
name="password_change",
),
]이 방식은 템플릿 위치와 URL 패턴을 프로젝트에 맞게 직접 정할 수 있다는 장점이 있다.
로그인 후 이동할 위치 정하기
사용자가 로그인한 뒤 어디로 이동할지도 설정해야 한다. settings.py에 다음 값을 추가할 수 있다.
LOGIN_REDIRECT_URL = "boards:post_list"
LOGOUT_REDIRECT_URL = "boards:post_list"
LOGIN_URL = "accounts:login"LOGIN_REDIRECT_URL은 로그인 성공 후 이동할 기본 주소다. LOGOUT_REDIRECT_URL은 로그아웃 후 이동할 주소다. LOGIN_URL은 로그인하지 않은 사용자가 보호된 페이지에 접근했을 때 보내질 로그인 페이지다.
예를 들어 글 작성 페이지에 로그인하지 않은 사용자가 접근하면 Django는 다음과 같은 주소로 이동시킬 수 있다.
/accounts/login/?next=/posts/create/여기서 next는 로그인 후 원래 가려던 페이지로 돌아가기 위한 값이다. Django의 기본 LoginView는 이 값을 처리해 준다.
함수 기반 View에서 접근 제한하기
함수 기반 View에서는 login_required 데코레이터를 사용한다. 데코레이터는 기존 함수 앞뒤에 기능을 덧붙이는 문법이다.
# boards/views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from .forms import PostForm
@login_required
def post_create(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect("boards:post_detail", pk=post.pk)
else:
form = PostForm()
return render(request, "boards/post_form.html", {"form": form})@login_required가 붙은 View는 로그인한 사용자만 실행할 수 있다. 로그인하지 않은 사용자는 LOGIN_URL로 이동한다.
여기서 중요한 코드는 form.save(commit=False)다. 폼에 입력된 제목과 내용만으로 바로 저장하지 않고, 아직 데이터베이스에 저장되지 않은 Post 객체를 먼저 만든다. 그런 다음 post.author = request.user로 작성자를 채운 뒤 post.save()를 호출한다.
작성자는 사용자가 폼에서 선택하게 만들면 안 된다. 그렇게 하면 다른 사람 이름으로 글을 작성하는 조작이 가능해진다. 작성자처럼 신뢰해야 하는 값은 서버에서 현재 로그인 사용자 기준으로 정해야 한다.
게시글 모델에 작성자 추가하기
게시글마다 작성자를 저장하려면 Post 모델에 사용자 관계를 추가해야 한다.
# boards/models.py
from django.conf import settings
from django.db import models
class Post(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="posts",
)
title = models.CharField(max_length=200)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)settings.AUTH_USER_MODEL은 현재 프로젝트에서 사용하는 사용자 모델을 가리킨다. 지금은 기본 User 모델을 사용하더라도 직접 User를 가져오기보다 이 설정을 사용하는 편이 안전하다. 나중에 사용자 모델을 바꾸더라도 코드 변경 범위를 줄일 수 있다.
모델을 바꿨으므로 마이그레이션을 만든 뒤 적용한다.
python manage.py makemigrations
python manage.py migrate이미 게시글 데이터가 있는 상태에서 author를 필수 필드로 추가하면 기존 글의 작성자를 어떻게 채울지 물을 수 있다. 학습 중이라면 데이터를 지우고 다시 시작할 수도 있지만, 실제 서비스에서는 기본 작성자를 지정하거나 임시로 null=True를 두고 단계적으로 데이터를 보정해야 한다.
클래스 기반 View에서 접근 제한하기
이전 장에서 게시판을 클래스 기반 View로 정리했다면 LoginRequiredMixin을 사용한다. 믹스인은 클래스에 특정 기능을 섞어 넣는 방식이다.
# boards/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import CreateView, UpdateView, DeleteView
from .models import Post
from .forms import PostForm
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
template_name = "boards/post_form.html"
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)LoginRequiredMixin은 반드시 CreateView보다 앞에 둔다. Python의 클래스 상속 순서 때문에 왼쪽에 있는 믹스인이 먼저 동작해야 접근 제한이 제대로 적용된다.
form_valid()는 폼 검증이 끝난 뒤 실제 저장으로 넘어가기 직전에 실행된다. 이 지점에서 form.instance.author에 현재 사용자를 넣으면 작성자 값이 함께 저장된다.
수정과 삭제에도 로그인 제한을 걸 수 있다.
class PostUpdateView(LoginRequiredMixin, UpdateView):
model = Post
form_class = PostForm
template_name = "boards/post_form.html"
class PostDeleteView(LoginRequiredMixin, DeleteView):
model = Post
template_name = "boards/post_confirm_delete.html"
success_url = reverse_lazy("boards:post_list")하지만 이것만으로는 부족하다. 로그인한 사용자라면 다른 사람의 글 수정 URL에 직접 접근할 수도 있기 때문이다. 로그인 여부와 작성자 여부는 다른 문제다.
작성자만 수정·삭제하게 만들기
가장 단순한 방법은 조회 범위를 현재 사용자의 글로 제한하는 것이다.
class PostUpdateView(LoginRequiredMixin, UpdateView):
model = Post
form_class = PostForm
template_name = "boards/post_form.html"
def get_queryset(self):
return Post.objects.filter(author=self.request.user)
class PostDeleteView(LoginRequiredMixin, DeleteView):
model = Post
template_name = "boards/post_confirm_delete.html"
success_url = reverse_lazy("boards:post_list")
def get_queryset(self):
return Post.objects.filter(author=self.request.user)UpdateView와 DeleteView는 URL의 pk로 수정하거나 삭제할 객체를 찾는다. 이때 get_queryset()의 결과 안에서만 찾게 만들면, 다른 사용자의 글은 없는 글처럼 처리된다. 보통 404 응답이 발생한다.
다른 방식으로는 UserPassesTestMixin을 사용할 수 있다.
from django.contrib.auth.mixins import UserPassesTestMixin
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
form_class = PostForm
template_name = "boards/post_form.html"
def test_func(self):
post = self.get_object()
return post.author == self.request.usertest_func()가 True를 반환하면 접근을 허용하고, False를 반환하면 거부한다. 단순히 작성자 글만 대상으로 한다면 get_queryset()으로 제한하는 방식이 간결하다. "스태프 사용자는 예외적으로 허용한다"처럼 조건이 복잡해지면 UserPassesTestMixin이 더 읽기 쉽다.
def test_func(self):
post = self.get_object()
return post.author == self.request.user or self.request.user.is_staff이렇게 하면 작성자이거나 관리자 사이트 접근 권한이 있는 스태프 사용자만 수정할 수 있다.
템플릿에서 사용자 상태 보여 주기
인증 기능을 만들면 화면도 사용자 상태에 따라 달라져야 한다. 공통 레이아웃인 base.html에 로그인 상태를 표시할 수 있다.
<nav>
<a href="{% url 'boards:post_list' %}">게시판</a>
{% if user.is_authenticated %}
<span>{{ user.username }}님</span>
<a href="{% url 'boards:post_create' %}">글쓰기</a>
<form action="{% url 'accounts:logout' %}" method="post" style="display:inline;">
{% csrf_token %}
<button type="submit">로그아웃</button>
</form>
{% else %}
<a href="{% url 'accounts:login' %}">로그인</a>
<a href="{% url 'accounts:signup' %}">회원가입</a>
{% endif %}
</nav>상세 페이지에서는 작성자에게만 수정·삭제 버튼을 보여 줄 수 있다.
{% if user == post.author %}
<a href="{% url 'boards:post_update' post.pk %}">수정</a>
<a href="{% url 'boards:post_delete' post.pk %}">삭제</a>
{% endif %}다만 템플릿에서 버튼을 숨기는 것만으로는 보안이 되지 않는다. 사용자가 주소를 직접 입력할 수 있기 때문이다. 템플릿은 사용자 경험을 위한 처리이고, 실제 권한 검사는 반드시 View에서 해야 한다.
인증과 권한 흐름 정리
이제 게시판은 사용자와 연결되었다. 회원가입한 사용자는 로그인할 수 있고, 로그인한 사용자만 글을 작성할 수 있다. 글을 저장할 때 작성자는 폼 입력이 아니라 request.user로 결정된다. 수정과 삭제는 LoginRequiredMixin으로 로그인 여부를 확인하고, get_queryset()이나 UserPassesTestMixin으로 작성자 여부를 검사한다.
Django 인증 시스템의 핵심은 세 가지다.
request.user로 현재 사용자를 알 수 있다.LoginView,LogoutView같은 기본 View로 인증 화면을 빠르게 연결할 수 있다.login_required와LoginRequiredMixin으로 보호할 페이지를 명확히 제한할 수 있다.
실전 서비스에서는 대부분의 기능이 사용자와 연결된다. 글, 댓글, 주문, 결제, 알림은 모두 "누가 만든 데이터인가"와 "누가 볼 수 있는가"를 따져야 한다. 인증은 로그인 화면 하나를 추가하는 일이 아니라, 데이터의 소유자와 접근 범위를 애플리케이션 전체에 반영하는 일이다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.