[ 글의 목적: django의 기본적인 user model에 대한 정보와 커스터마이징, 그리고 jwt token base 세팅까지의 기록 ]
django를 초기 세팅할때 대부분의 경우 user를 "관성적으로, 해왔던대로," 세팅을 많이 할 것이다. django에서 user의 full-custom을 위해 framework의 core를 제대로 알아야 해서 처음부터 모든 부분을 이해하기 쉽지 않다. 해당 부분을 정리하고 drf로 jwt token base auth 까지 기록하는 글이다. ( >= django 4.2 version )
django는 기본적으로 django.contrib.auth
을 통해 User model과 회원가입 등의 기능을 제공한다. (django.contrib.auth official docs) 하지만 기본 제공 기능과 필드에 절대 만족할 수 없다. 그렇기 때문에 거의 무조건 커스터마이징을 하게 된다.
User model
이 가지고 있는 기본적인 field와 attribute, method들은 위 docs에서 모두 확인할 수 있다. 기본적으로 django는 이 User model
베이스로 사용해야 auth를 포함한 프레임워크 레벨에서 제공하는 많은 기능을 활용할 수 있다. 그렇기때문에 적절하게 커스터마이징 하는 방법에 대해 다양한 방법이 있다.
기본적으로 위 4가지 방법을 제공하며 추천한다. django에서는 이 User model을 base로 인증에 SessionMiddleware 와 AuthenticationMiddleware 가 관여한다. django.contrib.sessions
애플리케이션을 통해 세션 관리 기능을 제공하며, 로그인 시 SessionMiddleware
가 세션을 처리하고 AuthenticationMiddleware
가 사용자 인증을 처리한다.
User
를 다루는 것은 어떤 application에서 정말 중요한 부분이다. 인증뿐 아니라 인가와 group, app 단위 또는 model의 transaction 단위 permission 등의 기능을 기본적으로 제공하는 django core를 사용하는 것은 엄청난 메리트가 있다. auth logic을 포함해 모든걸 커스터마이징하고 싶으면 그냥 django를 선택하지 않는 것을 추천한다.
사실 application을 만들땐 user 라는 model 보다, "인증과 인가"
가 훨씬 본질이다. model은 거들뿐
인증과 인가는 다르다. "인증"은 사용자가 자신이 주장하는 사람인지 확인하는 과정 이고, "인가"는 이미 인증된 사용자에게 특정 리소스나 기능에 접근하는 권한을 부여하거나 제한하는 과정 이다.
User
모델을 상속하지만 실제로 테이블엔 어떠한 변경도 없다. 즉 Proxy
모델은 실제로 새로운 데이터베이스 테이블을 만들지 않고 모델의 Python 레벨에서의 동작만 변경하는 것이다. from django.contrib.auth.models import User
from .managers import PersonManager
class Person(User):
objects = PersonManager()
class Meta:
proxy = True
ordering = ('first_name', )
def do_something(self):
...
User
모델을 상속한 Person
클래스를 정의한다. Meta
내부 클래스를 정의하면서 "프록시 모델 클래스" 임을 선언하고 정렬 순서를 first_name
기준으로 변경한다. (정렬 순서는 당연히 optional한 선택이다.)
do_something
같은 메소드를 추가할 수 있다. User.objects.all()
과 Person.objects.all()
코드는 스키마의 변경이 없으므로 "같은 쿼리로 동작"한다.
하지만 기존에 가지고 있는 field를 바꾸거나 attribute 자체를 추가하지는 못한다.
이 방법은 django user model 자체를 커스터마이징 한다기 보다는 user와 대응되는 신규 table을 만들고, signal을 활용한 연계를 하는 방법이 더 맞는 표현이다. Django Signal - 시그널, db.models.signal, Publish/Subscribe 매커니즘 글에서 signal에 대한 정보를 얻을 수 있다.
간단하게 표현하면 아래 코드들과 같다.
# model.py
from django.contrib.auth.models import User
from django.db import models
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
phone_number = models.CharField(max_length=15, blank=True)
address = models.TextField(blank=True)
def __str__(self):
return self.user.username
# signal.py
from django.db.models.signals import post_save
from django.dispatch import receiver
@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.profile.save()
User
인스턴스가 저장될 때마다 create_user_profile
함수를 호출하여 관련된 UserProfile
을 자동으로 생성하고, User
인스턴스가 저장될 때마다 관련된 UserProfile
도 함께 저장한다. AbstractUser
모델을 상속한 User
모델을 만든다. 그리고 기본적으로 django user model이 가지는 핵심 field(id / password / last_login / is_superuser / username / first_name / last_name / email / is_staff / is_active / date_joined) 도 같이 따라온다. 기존 user model의 app이 바뀌는 것이기 때문에 settings.py
에 참조를 수정해야 한다. # settings.py
AUTH_USER_MODEL = "user.User" # [app].[모델명]
# models.py
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
# 기본적으로 제공하는 필드 외에 원하는 필드를 적어준다.
nickname = models.CharField(max_length=50)
phone = models.PhoneNumberField(unique = True, null = False, blank = False)
특히 이렇게 user를 사용할 경우 "프로젝트 시작 전" 에 세팅하는 것이 좋다. 추후에 settings.AUTH_USER_MODEL
변경시 데이터베이스 스키마를 알맞게 재수정해야 하는데 사용자 모델 필드에 추가나 수정으로 끝나지 않고 완전히 새로운 사용자 객체를 생성하는 일이 된다.
그렇기 때문에 AUTH_USER_MODEL
을 지정하기 전에 manage.py migrate
등을 해버리면 완전 꼬인다 :) 이 방법은 기존 Django의 User 모델을 그대로 사용하므로 기본 로그인 인증 처리 부분은 Django의 것을 이용하면서 몇몇 사용자 정의 필드를 추가할 때 유용하다.
하지만 기본 Admin을 생각해보면, 기본적으로 from django.contrib.auth.admin import UserAdmin
을 사용하기 때문에 해당 Admin도 커스텀할 필요도 있다. 특히 field나 method or attribute 를 수정한 경우는 당연하게 말이다.
# admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
class CustomUserAdmin(UserAdmin):
# 필드 순서 및 구성을 커스터마이징
fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('profile_pic', 'website_url')}),
)
add_fieldsets = UserAdmin.add_fieldsets + (
(None, {
'classes': ('wide',),
'fields': ('profile_pic', 'website_url'),
}),
)
admin.site.register(CustomUser, CustomUserAdmin)
AbstractBaseUser
는 Django에서 제공하는 더 기본적인 추상 사용자 모델이다. 핵심적으로 필요한 필드와 메서드만 포함하는 완전히 새로운 사용자 모델을 생성할 수 있다. 그리고 인증과 관련된 핵심 기능만을 제공한다. 기본적으로 따라오는 field는 id / password / last_login 정도 밖에 되지 않는다. # models.py
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.db import models
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError("The Email field must be set")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
return self.create_user(email, password, **extra_fields)
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
date_joined = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
objects = CustomUserManager()
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
def __str__(self):
return self.email
이 경우 USERNAME_FIELD
로 사용자 이름으로 사용될 필드를 지정해야 하고, REQUIRED_FIELDS
로 createsuperuser
관리 명령을 사용할 때 필요한 추가 필드를 지정한다.
인증 및 권한 관련 메서드를 포함하는 CustomUserManager
매니저를 만들어서 구현해줘야 하고, 이 역시 settings.py
에서 AUTH_USER_MODEL
를 바꿔줘야 한다! 역시 admin
도 커스텀을 직접 해줘야 제대로 사용할 수 있다. (admin은 다음 글로 대체한다: 커스텀 User 모델 (AbstractBaseUser의 상속))
PermissionsMixin
은 권한 관련 필드와 메서드 (is_superuser, groups, user_permissions 등)를 제공하기 때문에, 해당 Mixin
을 상속 받은 뒤 따로 커스텀을 하던지, 일단 상속을 받아야 기본 제공 권한 method를 활용할 수 있다.
http request (state-less 등)와 RESTful API 에 대한 설명, 그리고 jwt token 저장하는 위치에 대해서는 다루지 않는다. drf로 jwt token based auth를 바로 구현해보자!
Session
, Token
, JWT
이 있으며 기본적으로 session 방식을 채택하고 있다. 무엇을 왜 선택하는가? 에 대한 얘기는 인프라와 트래픽의 관점도 있기 때문에 무엇이 정답이다! 라고 할 수 없다. 세션 vs 토큰 vs 쿠키? 기초개념 잡아드림. 10분 순삭! 의 링크로 해당 내용을 대체한다.먼저 이 글을 읽으면 도움이 많이 된다. - JWT(Json Web Token) 알아가기. 이 글은 token에 초점이 맞춰진게 아니라 구현이 더 중심이라 jwt token에 대한 상세한 설명은 skip 하겠다.
jwt를 간단하게 한 문장으로 표현하면 "JSON Web Tokens의 약자이며 인증에 필요한 정보를 담아 암호화 시켜 사용하는 token" 이다.
헤더 (Header)
, 페이로드 (Payload)
, 서명 (Sinature)
로 구성된다. Base64 인코딩의 경우 “+”, “/”, “=”이 포함되지만 JWT는 URI에서 파라미터로 사용할 수 있도록 URL-Safe 한 Base64url 인코딩을 사용합니다.header
에는 보통 토큰의 타입이나, 서명 생성에 어떤 알고리즘이 사용되었는지 저장한다. payload
에는 보통 Claim
이라는 사용자에 대한, 혹은 토큰에 대한 property를 "key-value" 의 형태로 저장한다. Claim
이라는 말 그대로 토큰에서 사용할 정보의 조각을 의미하며 사실 어떤 Claim값을 넣는지는 개발자의 선택이긴 하지만 JWT의 표준 스펙이 있다. 크게 "등록된 (registered) 클레임", "공개 (public) 클레임", "비공개 (private) 클레임" 으로 세 종류가 있다.마지막 signature
는 "secret key" 를 포함하여 암호화되어 있다. 서버에 있는 개인키로만 암호화를 풀 수 있으니 다른 client는 임의로 signature
를 복호화 할 수 없다. 위에서 살펴본 header
& payload
값은 서버가 가지고 있는 "your-256-bit-secret"를 가지고 암호화 되어 있다.
실제 이렇게 구현된 jwt token은 인가와 인증과정에서 아래 2가지를 활용한다.
access token
: 매번 인가를 받을 때 사용하는 토큰, 수명 짧음refresh token
: access token의 수명이 다했을 때 access token을 "재발행" 받기 위한 토큰, 수명 김라이브러리는 drf(djangorestframework)
, simple-jwt(djangorestframework-simplejwt)
를 사용할 것이다. 일단 python manage.py startapp user
user app 추가해서 시작한다. (해당 부분은 본인에게 편한대로)
회원가입 부터 시작해서 로그인까지, "jwt token" 기반의 방식으로 진행하고, 회원가입시 "access"와 "refresh" token 모두 받을 수 있게 구성해보자.
# settings.py
AUTH_USER_MODEL = "user.User" # for get Auth user model
INSTALLED_APPS = [
...
"rest_framework", # djangorestframework
"rest_framework_simplejwt", # djangorestframework-simplejwt
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
...
}
# 추가적인 JWT_AUTH 설정, https://django-rest-framework-simplejwt.readthedocs.io/en/latest/
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"ROTATE_REFRESH_TOKENS": True, # True로 설정할 경우, refresh token을 보내면 새로운 access token과 refresh token이 반환된다.
"BLACKLIST_AFTER_ROTATION": True, # True로 설정될 경우, 기존에 있던 refresh token은 blacklist가된다
"UPDATE_LAST_LOGIN": True,
"ALGORITHM": "HS256",
"SIGNING_KEY": SECRET_KEY,
"VERIFYING_KEY": None,
"AUDIENCE": None,
"ISSUER": None,
"JWK_URL": None,
"LEEWAY": 0,
"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
"JTI_CLAIM": "jti",
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}
...
SIMPLE_JWT
관련 설정은 링크해둔 official docs 에서 꼭 디테일 설정값을 체크하길 바란다. 이제 user app의 user model을 정의해 보자!from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.models import PermissionsMixin
from django.db import models
class UserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError("Users must have an email address")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password, **extra_fields):
return self.create_user(
email,
password=password,
is_staff=True,
is_superuser=True,
is_active=True,
**extra_fields,
)
class User(AbstractBaseUser, PermissionsMixin):
"""
- User table model
"""
email = models.EmailField( # 사용자 ID (email format)
verbose_name="email address",
max_length=255,
unique=True,
)
name = models.CharField(max_length=50) # 사용자 이름
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True) # 활성 비활성 - 탈퇴시 비활성 (core)
created_at = models.DateTimeField(auto_now_add=True)
objects = UserManager() # 재정의 된 UserManager 사용 선언
USERNAME_FIELD = "email" # email을 ID 필드로 사용 선언
REQUIRED_FIELDS = ["name"] # 사용자 이름은 필수 필드
def __str__(self):
return self.email
class Meta:
app_label = "user"
위에서 살펴보고 언급한 AbstractBaseUser
를 사용할 것이고, 필수 필드인 USERNAME_FIELD
와 필요한 경우 REQUIRED_FIELDS
를 정의해야 한다. 그리고 is_staff
값을 django core에서 활용하는 값이기 때문에 남겨두자! 그리고 manager 역시 정의 해줘야 한다.
BaseUserManager
를 상속받아 사용자 모델의 manager를 커스터마이징 할 때는 create_user
및 create_superuser
메서드를 정의해야 한다. 당연히 User model의 field들이 모두 달라졌으니 해당 manager를 통해 어떻게 기본 user를 만들고 superuser 를 만들지 명시해줘야 한다.
urls.py
는 skip하고 view와 serializer의 코드는 한 눈에 살펴보자!
# serializers.py
from django.contrib.auth import get_user_model
from rest_framework import serializers
User = get_user_model()
class UserRegisterSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
extra_kwargs = {"password": {"write_only": True}}
def create(self, validated_data):
return User.objects.create_user(**validated_data)
class UserLoginSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
# views.py
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView, status
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.tokens import Token
from .serializers import UserLoginSerializer, UserRegisterSerializer
class UserRegisterAPIView(APIView):
def post(self, request: Request):
serializer = UserRegisterSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
token: Token = TokenObtainPairSerializer.get_token(user)
res = Response(
{
"user": serializer.data,
"message": "register successs",
"token": {
"access": str(token.access_token),
"refresh": str(token),
},
},
status=status.HTTP_200_OK,
)
return res
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class UserLoginAPIView(APIView):
def post(self, request: Request):
token_serializer = TokenObtainPairSerializer(data=request.data)
if token_serializer.is_valid():
user = token_serializer.user
serializer = UserLoginSerializer(user)
return Response(
{
"user": serializer.data,
"message": "login success",
"token": token_serializer.validated_data,
},
status=status.HTTP_200_OK,
)
return Response(token_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
User = get_user_model()
은 많이 사용하는 user 모델인 만큼 "순환 참조를 막기" 위해 많이 사용한다. 해당 함수는 AUTH_USER_MODEL = "user.User"
에 설정한 값으로 user model를 가져와 준다. get_user_model vs settings.AUTH_USER_MODEL
simple-jwt(djangorestframework-simplejwt)
가 제공하는 기본 serializer는 회원가입시 refresh를 주지 않는다. 그래서 UserRegisterAPIView
에서 직접 회원가입을 만들어 TokenObtainPairSerializer
활용해 response를 다시 만들었다. 쿠키값 세팅이 필요하다면 res.set_cookie("access_token", access_token, httponly=True, samesite='Strict')
등 을 활용하면 된다.
가입시 get_token
으로 token 발급을 직접한다. 이 경우 validated_data
가 없기때문에 직접 Token
object로 부터 받아와야 한다.
Login
에 TokenObtainPairSerializer
를 먼저하는 이유는 "이미 로그인 요청의 유효성 검사, 사용자 인증" 을 포함하고 있기 때문이다. 그리고 "UPDATE_LAST_LOGIN": True,
로 세팅했다면 signal로 추상 model이 가지고 있는 last_login
값을 업데이트 한다. 이 경우 DB 병목이 있을 수 있으니 False
로 세팅하는 포인트를 생각해두는게 좋다. res cookie 얘기는 위로 대체한다.
jwt token 특성상 logout은 굳이 API로 구현할 필요가 없다. FE에서 직접 token을 삭제(쿠키 해제 등)를 하는게 더 좋다.
from django.contrib.auth.admin import UserAdmin
를 상속받아 오버라이딩 했다.from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin
from .models import User
class UserAdmin(DefaultUserAdmin):
# list display
list_display = (
"email",
"name",
"is_active",
"is_staff",
"is_superuser",
"last_login",
)
list_filter = ("is_active", "is_staff", "is_superuser")
search_fields = ("email", "name")
ordering = ("email",)
# detail display
readonly_fields = ("created_at",)
fieldsets = (
(None, {"fields": ("email", "password")}),
("Personal info", {"fields": ("name",)}),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
(
"Important dates",
{
"fields": (
"last_login",
"created_at",
)
},
),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("email", "name", "password1", "password2"),
},
),
)
admin.site.register(User, UserAdmin)
settings.py
에서 DEFAULT_AUTHENTICATION_CLASSES
를 바꿔주었기 때문에 그대로 drf permission을 가져다가 쓰면 된다. 아래 ping이라는 API 예시를 보자.from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView, status
class PingAPIView(APIView):
permission_classes = [IsAuthenticated]
def get(self, requset: Request):
return Response(status=status.HTTP_200_OK)
401
을 뱉을것이고, header에 Authorization: Bearer <token...>
세팅해서 보내면 2번째 사진처럼 정상적으로 받을 수 있다.TokenRefreshView
를 사용하면 편하게 만들 수 있다. # user app의 urls.py
from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
from .views import UserLoginAPIView, UserRegisterAPIView
urlpatterns = [
path("sign-up/", UserRegisterAPIView.as_view(), name="user-sign-up"),
path("sign-in/", UserLoginAPIView.as_view(), name="user-sign-in"),
path("refresh/", TokenRefreshView.as_view(), name="user-token-refresh"),
]
어딘가에는 access token과 refresh token을 저장해야 하고, 해당 방식에 가장 흔하게 볼 수 있는 구현이 "http cookie" 이다. view 로직에는 해당 부분이 빠졌지만 언급한 바와 같이 res.set_cookie(...)
로 간단하게 만들 수 있다. Access Token과 Refresh Token을 어디에 저장해야 할까? 글을 추천한다.
FE에서는 기본적으로 구현 방향이 [ API call -> 401? -> refresh API call -> (if 200) API call again / (if not 200) login page]
가 된다. 물론 BE에서 refresh 로직을 가져갈 수 있다.
permission 은 drf에서 permission 구현하듯이 진행하면 똑같이 사용가능하다. 예를 들면 아래와 같다.
from rest_framework.permissions import BasePermission, IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView, status
class IsTargetEmail(BasePermission):
def has_permission(self, request, view):
# 사용자가 인증되지 않았다면 False 반환
if not request.user or not request.user.is_authenticated:
return False
# 사용자의 이메일의 도메인이 "example.com"인지 확인
return "example.com" == request.user.email.split("@")[-1]
class PingAPIView(APIView):
permission_classes = [IsTargetEmail]
def get(self, requset: Request):
return Response(status=status.HTTP_200_OK)
이제 drf와 써드파티로 jwt token based user sign-up & sign-in을 확인했다. 더 나아가 social login까지 이어서 구현해보자!
안녕하세요! 좋은 글 작성해주셔서 감사합니다.
글을 읽으며 궁금한 사항이 생겨 질문 남깁니다.
rest_framework_simplejwt 라이브러리에서 제공하는 TokenRefreshView에서도 권한 검증(인증)이 이루어지는지 궁금합니다.
구체적으로 질문드리자면, TokenRefreshView는 Refresh Token을 입력하면 Access Token을 재발급하여 반환하는 API인데, 해당 API에서 요청을 보낸 유저와 Refresh Token의 유저 정보가 일치하는지 검증이 이루어지는지 궁금합니다.
검증이 이루어지지않는다면, TokenRefreshView를 상속받아 유저 검증을 하는 로직을 추가하여 구현해야할까요?