장고 중급 #4 사용자/권한 — 커스텀 user model, permission, group
기초 #7에서 장고의 빌트인 인증을 다졌습니다 — 로그인/로그아웃, @login_required, User 모델. 실무에 들어가면 곧 두 가지 질문을 만납니다.
- “username 대신 이메일로 로그인하고 싶은데?”
- “사용자에게 세밀한 권한을 주려면?”
이 글이 그 답입니다. 한 가지 무거운 경고로 시작합니다 — AUTH_USER_MODEL은 프로젝트 시작 시점에 정해야 합니다.
무거운 경고 — 시작 시점에 정해야 할 결정 #
장고 공식 문서가 강조하는 한 줄: “Even if you’re happy with the default User model, you’ll likely want to change it later. So if you’re starting a new project, definitely set up a custom user model, even if it looks just like Django’s default user model.”
(번역: 기본 User 모델로 만족스럽더라도 나중에 바꾸고 싶을 가능성이 높습니다. 새 프로젝트라면 기본 User와 똑같이 생긴 것이라도 반드시 커스텀 user model로 시작하세요.)
이유는 마이그레이션 후에 바꾸기가 매우 어렵기 때문입니다. 모든 외래키, 모든 마이그레이션 히스토리, 모든 데이터를 옮겨야 합니다. 새 프로젝트에선 다음 두 줄을 첫 마이그레이션 전에 잡으세요.
AUTH_USER_MODEL = "accounts.User"from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass지금 당장 추가 필드가 없어도 자기 User 클래스를 가지고 시작합니다. 나중에 필드를 추가하기는 쉽기 때문입니다.
AbstractUser vs AbstractBaseUser
#
커스터마이즈 깊이에 따라 두 갈래가 있습니다.
AbstractUser | AbstractBaseUser + PermissionsMixin | |
|---|---|---|
| 기본 필드 | username, email, first_name, last_name, … | password, last_login만 |
| 권한 시스템 | 자동 포함 | PermissionsMixin으로 추가 |
| 추천 용도 | 대부분 (90%) — 약간만 추가 | 완전 다른 사용자 모델 (이메일만 등) |
| 작업량 | 매우 적음 | 많음 (UserManager 직접 작성) |
거의 모든 경우에 AbstractUser가 답. 정말 username 자체를 없애고 이메일만 쓰고 싶을 때만 AbstractBaseUser.
AbstractUser — 필드 추가만
#
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to="avatars/", blank=True, null=True)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return self.username이게 끝입니다. username, email, first_name, last_name, is_staff, is_active 등은 그대로 살아 있습니다.
이메일 로그인 패턴 — AbstractBaseUser
#
실무에서 자주 만나는 요구 — 이메일을 username으로 쓰기. 이때는 AbstractBaseUser + 커스텀 매니저 조합이 표준입니다.
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.db import models
from django.utils import timezone
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra):
if not email:
raise ValueError("이메일은 필수입니다.")
email = self.normalize_email(email)
user = self.model(email=email, **extra)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra):
extra.setdefault("is_staff", False)
extra.setdefault("is_superuser", False)
return self._create_user(email, password, **extra)
def create_superuser(self, email, password=None, **extra):
extra.setdefault("is_staff", True)
extra.setdefault("is_superuser", True)
if extra.get("is_staff") is not True:
raise ValueError("superuser는 is_staff=True 여야 합니다.")
if extra.get("is_superuser") is not True:
raise ValueError("superuser는 is_superuser=True 여야 합니다.")
return self._create_user(email, password, **extra)
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
name = models.CharField(max_length=100, blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(default=timezone.now)
objects = UserManager()
USERNAME_FIELD = "email" # 로그인에 쓸 필드
REQUIRED_FIELDS = [] # createsuperuser가 묻는 추가 필드 (email/password 외)
def __str__(self):
return self.email체크포인트:
USERNAME_FIELD = "email"— 로그인 식별자.unique=True필수REQUIRED_FIELDS—createsuperuser명령이 추가로 물어볼 필드.USERNAME_FIELD와password는 자동 포함이라 여기 적지 않음- **
UserManager**의create_user/create_superuser가manage.py createsuperuser등에서 호출됨 - **
PermissionsMixin**을 같이 상속해야 그룹/퍼미션 시스템이 동작
admin 등록 #
createsuperuser가 동작해도 admin의 사용자 변경 폼은 기본 User 기준이라 깨집니다. UserAdmin을 직접 등록해야 합니다.
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import User
class UserAdmin(BaseUserAdmin):
ordering = ("email",)
list_display = ("email", "name", "is_staff", "is_active")
search_fields = ("email", "name")
fieldsets = (
(None, {"fields": ("email", "password")}),
("개인 정보", {"fields": ("name",)}),
("권한", {"fields": ("is_active", "is_staff", "is_superuser",
"groups", "user_permissions")}),
("기록", {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(None, {"classes": ("wide",),
"fields": ("email", "password1", "password2")}),
)
admin.site.register(User, UserAdmin)회원가입/로그인 폼도 username 기반의 빌트인 폼을 그대로 못 쓰니, UserCreationForm / AuthenticationForm을 상속해 이메일 기반으로 다시 만듭니다.
Permission 시스템 #
장고는 모델당 자동으로 4 개의 퍼미션을 만듭니다.
| 퍼미션 코드 | 의미 |
|---|---|
add_<model> | 추가 |
change_<model> | 수정 |
delete_<model> | 삭제 |
view_<model> | 조회 |
blog 앱의 Post 모델이라면 blog.add_post, blog.change_post, blog.delete_post, blog.view_post가 자동 생성됩니다.
코드에서 검사 #
if request.user.has_perm("blog.change_post"):
# 수정 권한 있음
...
if request.user.has_perms(["blog.change_post", "blog.delete_post"]):
# 둘 다 있음
...데코레이터 / Mixin #
from django.contrib.auth.decorators import permission_required
@permission_required("blog.change_post", raise_exception=True)
def post_edit(request, pk):
...from django.contrib.auth.mixins import PermissionRequiredMixin
class PostUpdateView(PermissionRequiredMixin, UpdateView):
permission_required = "blog.change_post"
raise_exception = True#1 CBV에서 본 그 Mixin입니다.
커스텀 퍼미션 #
모델 Meta에 정의해서 자동 생성에 추가할 수 있습니다.
class Post(models.Model):
...
class Meta:
permissions = [
("publish_post", "Can publish post"),
("feature_post", "Can feature post on homepage"),
]makemigrations 후 blog.publish_post로 사용합니다. admin의 사용자/그룹 편집에서 부여할 수 있습니다.
Group — 퍼미션 묶음 #
권한을 사용자에게 직접 주면 관리가 어렵습니다. Group으로 묶어서 사용자에게 부여하는 게 표준입니다.
from django.contrib.auth.models import Group, Permission
editors, _ = Group.objects.get_or_create(name="에디터")
editors.permissions.add(
Permission.objects.get(codename="add_post"),
Permission.objects.get(codename="change_post"),
Permission.objects.get(codename="publish_post"),
)
# 사용자에게 그룹 부여
user.groups.add(editors)admin 페이지의 그룹 메뉴에서도 같은 일을 할 수 있습니다. 보통 마이그레이션이나 시드 명령으로 그룹/퍼미션 초기값을 코드에 적어두는 게 재현성 측면에서 좋습니다.
from django.db import migrations
def create_default_groups(apps, schema_editor):
Group = apps.get_model("auth", "Group")
Permission = apps.get_model("auth", "Permission")
editors, _ = Group.objects.get_or_create(name="에디터")
perms = Permission.objects.filter(
codename__in=["add_post", "change_post", "publish_post"]
)
editors.permissions.set(perms)
class Migration(migrations.Migration):
dependencies = [("blog", "0001_initial")]
operations = [migrations.RunPython(create_default_groups, migrations.RunPython.noop)]템플릿에서 그룹/퍼미션 검사 #
{% if user.is_authenticated %}
{% if perms.blog.publish_post %}
<a href="{% url 'post_publish' post.pk %}">발행</a>
{% endif %}
{% endif %}perms는 django.contrib.auth.context_processors.auth 컨텍스트 프로세서가 넘기는 헬퍼입니다. settings.py의 TEMPLATES.OPTIONS.context_processors에 들어 있으면 자동.
Object-level permission — 한 줄 안내 #
장고 빌트인 퍼미션은 모델 단위 입니다 — “이 사용자가 Post를 수정할 수 있는가” 까지만. “이 사용자가 이 특정 Post를 수정할 수 있는가” (객체 단위)는 없습니다.
객체 단위 권한이 필요하면:
UserPassesTestMixin(#1)로 직접 검사 — 작은 경우엔 충분- django-guardian 외부 라이브러리 — 본격 객체 권한
- DRF의
permission_classes— REST API 라면 (실전 #2)
작은 경우엔 직접 검사가 깔끔합니다.
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
form_class = PostForm
def test_func(self):
return self.get_object().author == self.request.user비밀번호 — 해시 / 변경 / 재설정 #
장고는 비밀번호를 PBKDF2 (기본) 등 안전한 해시로 자동 저장합니다. 직접 만질 일은 거의 없습니다.
user.set_password("새비밀번호") # 해시해서 저장
user.save()
user.check_password("입력값") # 검증평문 비밀번호를 저장하지 마세요. user.password = "..."로 직접 대입하면 평문이 들어가니 반드시 set_password를 사용해야 합니다.
빌트인 패스워드 변경/재설정 뷰는 django.contrib.auth.urls에 들어있습니다.
from django.urls import path, include
urlpatterns = [
path("accounts/", include("django.contrib.auth.urls")),
# /accounts/login/, /accounts/logout/, /accounts/password_change/,
# /accounts/password_reset/ 등
]DRF의 인증과의 차이 — 짧게 #
이번 글의 인증은 세션 + 쿠키 (브라우저 폼 로그인) 모델입니다. REST API는 보통 토큰 / JWT / OAuth2 등 stateless 인증을 씁니다.
| 장고 빌트인 (이번 글) | DRF | |
|---|---|---|
| 인증 매체 | 세션 쿠키 | Token / JWT / OAuth2 |
| 권한 검사 | has_perm / Mixin / 데코레이터 | permission_classes |
| 사용자 모델 | 동일 (AUTH_USER_MODEL) | 동일 |
| 로그인 폼 | 빌트인 HTML | 별도 엔드포인트 |
같은 User 모델을 쓰지만 인증 매체와 권한 검사 성격이 다른 구조라고 보면 됩니다. DRF의 인증/권한 스택은 실전 #2에서 자세히 다루겠습니다.
request.user의 정체
#
AuthenticationMiddleware (#3에서 본 그 미들웨어)가 모든 요청에 request.user를 채워 넣습니다.
- 로그인 됐으면 →
User인스턴스 - 안 됐으면 →
AnonymousUser(가짜 객체.is_authenticated == False)
if request.user.is_authenticated:
...
if request.user.is_staff:
...
if request.user.is_superuser:
...is_authenticated는 속성 입니다 (메소드 아님). request.user.is_authenticated()로 호출하면 안 됩니다 — 옛 장고 1.x 시절엔 메소드였지만 지금은 속성.
정리 #
이번 글에서 잡은 것:
- **
AUTH_USER_MODEL**은 프로젝트 시작 시점에 잡기 — 나중 변경은 매우 비쌈 - 거의 모든 경우에 **
AbstractUser**가 답 (필드만 추가) - 이메일 로그인 —
AbstractBaseUser+PermissionsMixin+ 커스텀UserManager USERNAME_FIELD,REQUIRED_FIELDS의 의미- 모델당 자동 4개 퍼미션 (
add/change/delete/view_<model>) has_perm,@permission_required,PermissionRequiredMixin- 모델
Meta.permissions로 커스텀 퍼미션 - Group으로 권한 묶기 — 사용자에게는 그룹을 부여
- 객체 단위 권한은
UserPassesTestMixin또는django-guardian/ DRF - 비밀번호는
set_password/check_password, 빌트인 변경/재설정 뷰 request.user.is_authenticated는 속성
다음 글(#5 메시지/세션/쿠키)에서는 인증된 사용자에게 보내는 flash 메시지, 요청 사이의 상태 저장을 위한 세션, 그리고 그 밑단의 쿠키 까지를 한 글에 풉니다.