Skip to content

Django Custom User with DRF and Simple JWT

Django 內建 User Model 供認證,但官方文件強烈建議在專案一開始就使用自訂的 User Model。因為未來若對 User Model 有調整需求,例如增加欄位,都只能使用自訂的 User Model,在一開始就使用內建的 User Model 會因為牽涉到各種 foreign ken 與多對多關係導致難以遷移(Changing to a custom user model mid-project)。

官方文件提供了一個比較複雜的 Custom User 建立方式,但使用 AbstractBaseUser 或是 AbstractUser 是大家比較常用而且也相對簡單的方式。AbstractBaseUser 需要補充許多 User Model 的實作細節,而 AbstractUser 是 AbstractBaseUser 的 subclass,已經實作出一個完整的 User Model,單純只是調整欄位使用 AbstractUser 是最簡便的方式。

本範例使用 Django REST framework(DRF) 做 API,並利用 Simple JWT 進行驗證。完整範例請參考 Django Custom User with DRF and Simple JWT

Setup

  1. 使用 virtualenv 建立虛擬環境
  2. 安裝 package
  3. 初始 Django project website
  4. 建立 Django application account 用於放置 Custom User Model
$ mkdir django-custom-user && cd django-custom-user
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install Django==3.1.4 djangorestframework==3.12.2 djangorestframework-simplejwt==4.6.0
(.venv) $ django-admin.py startproject website .
(.venv) $ python manage.py startapp account
(.venv) $ python manage.py runserver

Custom User Model

  1. 建立 Custom User Model
  2. 更新 website/settings.py,將 User Model 改為 Custom User Model
  3. 執行 makemigrationsmigrate 更新 DB
  4. 建立 superuser admin 帳號

account/models.py 新增我們要用的 Custom User Model,這時就可以增加需要的欄位,例如生日等。

# account/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models


class CustomUser(AbstractUser):
    birthday = models.DateField(null=True, blank=True, default=None)
    # add additional fields in here

    def __str__(self):
        return self.username

website/settings.py 需要加入我們增加的 account application,並增加 AUTH_USER_MODEL 設為我們新建的 Custom User Model,取代原本內建的 User Model。

# website/settings.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'account.apps.AccountConfig', # new
]
...
AUTH_USER_MODEL = "account.CustomUser"

完成 AUTH_USER_MODEL 的設定後就可以執行首次的 makemigrationsmigrate,在調整 AUTH_USER_MODEL 前切勿執行!

(.venv) $ python manage.py makemigrations
(.venv) $ python manage.py migrate

建立 superuser 之後用於驗證。

(.venv) $ python manage.py createsuperuser

Django REST framework

  1. 新增 account/serializers.py, account/views.pyaccount/urls.py
  2. 更新 website/settings.pywebsite/urls.py

序列化查詢結果,除了 Customer User Model 外也增加了內建的 Group Model。輸出的 fields 設定為全部欄位,可依需求調整。

# account/serializers.py
from .models import CustomUser
from django.contrib.auth.models import Group
from rest_framework import serializers


class CustomUserSerializer(serializers.ModelSerializer):
    class Meta:
        model = CustomUser
        fields = "__all__"


class GroupSerializer(serializers.ModelSerializer):
    class Meta:
        model = Group
        fields = "__all__"

設定 View 作為 API interface。

# account/views.py
from .models import CustomUser
from django.contrib.auth.models import Group
from rest_framework import viewsets
from rest_framework import permissions
from .serializers import CustomUserSerializer, GroupSerializer


class CustomUserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """

    queryset = CustomUser.objects.all().order_by("-date_joined")
    serializer_class = CustomUserSerializer
    permission_classes = [permissions.IsAuthenticated]


class GroupViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows groups to be viewed or edited.
    """

    queryset = Group.objects.all()
    serializer_class = GroupSerializer
    permission_classes = [permissions.IsAuthenticated]

將 View 註冊到 router 中。

# account/urls.py
from django.urls import include, path
from rest_framework import routers

from . import views

router = routers.DefaultRouter()
router.register(r"account/user", views.CustomUserViewSet)
router.register(r"account/group", views.GroupViewSet)

urlpatterns = [
    path("", include(router.urls)),
]

website/settings.pyINSTALLED_APPS 增加 rest_framework

# website/settings.py
...
INSTALLED_APPS = [
    ...
    "rest_framework", # new
]
...

include 前面在 account 中設定的 url,並增加 api-auth 用於通過 DRF 的內建網頁認證。

# website/urls.py
from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("account.urls")), # new
    path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), # new
]

完成後就可以使用 curl 測試 API,帳號使用先前建立的 superuser。

$ curl -H 'Accept: application/json; indent=4' -u admin:admin http://127.0.0.1:8000/api/account/user/
[
    {
        "id": 1,
        "password": "pbkdf2_sha256$216000$fHERFJ7toIcI$3QOXbA4or+srGXn+60aW+z4rslvJkQcW2wS0oWWzYHI=",
        "last_login": "2020-12-07T15:46:50.478483Z",
        "is_superuser": true,
        "username": "admin",
        "first_name": "",
        "last_name": "",
        "email": "admin@sample.com",
        "is_staff": true,
        "is_active": true,
        "date_joined": "2020-12-07T13:54:21.479125Z",
        "birthday": null,
        "groups": [],
        "user_permissions": []
    }
]

或是開起 DRF 內建的網頁 localhost:8000/api/account/user/,如下圖:

DRF

從結果可以確認我們使用的 Custom User 有新增的 Birthday 欄位,superuser 也是建立在 Custom User Model 中。

Simple JWT

  1. 更新 website/settings.py,調整預設認證 class
  2. 更新 website/urls.py,增加 Token 相關 API

設定 REST_FRAMEWORKDEFAULT_AUTHENTICATION_CLASSES 為 Simple JWT。

# website/settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    )
}

增加取得 Token 與更新 Token 的兩個 API。

# website/urls.py
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    ...
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    ...
]

完成後可以使用先前建立的 superuser 取得 Token。

$ curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "admin"}' \
  http://localhost:8000/api/token/

{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYwNzQ0MzQ3NiwianRpIjoiYzQzYjM2Zjg2ODA0NDU1MzliYzUwNTlmN2YzN2NkMTEiLCJ1c2VyX2lkIjoxfQ.ZC0fAj7HR99v_po4BI-uVVeS9c7ZoN4B35_pYzosE_o","access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjA3MzU3Mzc2LCJqdGkiOiIyYjY5NzkxMmZhNjE0NzY3YmJkNDA2NjExMzE1YzkxMCIsInVzZXJfaWQiOjF9.92m-V9vjRxUWGLlcJRFBdLqSHp0UII3SLPTt_yPynqY"}

將取得的 Token 加入 Header 中,再次測試 User 清單的 API。

$ curl -H 'Accept: application/json; indent=4' -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjA3MzU3Mzc2LCJqdGkiOiIyYjY5NzkxMmZhNjE0NzY3YmJkNDA2NjExMzE1YzkxMCIsInVzZXJfaWQiOjF9.92m-V9vjRxUWGLlcJRFBdLqSHp0UII3SLPTt_yPynqY" http://127.0.0.1:8000/api/account/user/
[
    {
        "id": 1,
        "password": "pbkdf2_sha256$216000$fHERFJ7toIcI$3QOXbA4or+srGXn+60aW+z4rslvJkQcW2wS0oWWzYHI=",
        "last_login": "2020-12-07T16:02:06.847562Z",
        "is_superuser": true,
        "username": "admin",
        "first_name": "",
        "last_name": "",
        "email": "admin@sample.com",
        "is_staff": true,
        "is_active": true,
        "date_joined": "2020-12-07T13:54:21.479125Z",
        "birthday": null,
        "groups": [],
        "user_permissions": []
    }
]

Reference:

  1. Django Custom User Model
  2. 自訂 Django User Model 的三種方式
  3. Django REST framework
  4. SimpleJWT Document

Comments