在 Django 中自訂驗證

Django 內建的驗證功能已足夠應付大多數常見情況,但您可能會有一些預設值無法滿足的需求。在您的專案中自訂驗證需要了解所提供系統的哪些點是可擴展或可替換的。本文件詳細說明如何自訂驗證系統。

驗證後端提供一個可擴展的系統,用於當需要針對 Django 預設以外的不同服務驗證儲存在使用者模型中的使用者名稱和密碼時。

您可以給您的模型自訂權限,這些權限可以透過 Django 的授權系統進行檢查。

您可以擴展預設的 User 模型,或者替代一個完全自訂的模型。

其他驗證來源

有時您可能需要掛接到另一個驗證來源 – 也就是其他使用者名稱和密碼或驗證方法的來源。

例如,您的公司可能已經有一個 LDAP 設定,為每位員工儲存使用者名稱和密碼。如果使用者在 LDAP 和基於 Django 的應用程式中擁有單獨的帳戶,對於網路管理員和使用者本身來說都會很麻煩。

因此,為了處理這種情況,Django 驗證系統可讓您插入其他驗證來源。您可以覆蓋 Django 基於預設資料庫的方案,或者您可以將預設系統與其他系統一起使用。

請參閱驗證後端參考,以了解 Django 包含的驗證後端的資訊。

指定驗證後端

在幕後,Django 維護一個「驗證後端」的列表,它會檢查這些後端以進行驗證。當有人呼叫 django.contrib.auth.authenticate() 時 – 如如何登入使用者中所述 – Django 會嘗試在其所有驗證後端上進行驗證。如果第一個驗證方法失敗,Django 會嘗試第二個方法,依此類推,直到嘗試過所有後端。

要使用的驗證後端列表在 AUTHENTICATION_BACKENDS 設定中指定。這應該是指向知道如何驗證的 Python 類別的 Python 路徑名稱列表。這些類別可以位於您 Python 路徑上的任何位置。

預設情況下,AUTHENTICATION_BACKENDS 設定為

["django.contrib.auth.backends.ModelBackend"]

這是基本的驗證後端,它會檢查 Django 使用者資料庫並查詢內建權限。它沒有提供透過任何速率限制機制來防止暴力攻擊的保護。您可以實作自己的速率限制機制在自訂驗證後端中,或使用大多數網頁伺服器提供的機制。

AUTHENTICATION_BACKENDS 的順序很重要,因此如果相同的使用者名稱和密碼在多個後端中有效,Django 會在第一個正向匹配時停止處理。

如果後端引發 PermissionDenied 例外,驗證會立即失敗。Django 不會檢查後續的後端。

注意

一旦使用者通過驗證,Django 會在使用者會話中儲存用於驗證使用者的後端,並且只要需要存取目前經過驗證的使用者,就會在該會話期間重複使用相同的後端。這實際上表示驗證來源是基於每個會話進行快取的,因此如果您更改 AUTHENTICATION_BACKENDS,如果您需要強制使用者使用不同的方法重新驗證,則需要清除會話資料。一種簡單的方法是執行 Session.objects.all().delete()

撰寫驗證後端

驗證後端是一個類別,它實作兩個必要的方法:get_user(user_id)authenticate(request, **credentials),以及一組可選的權限相關授權方法

get_user 方法接受一個 user_id – 可以是使用者名稱、資料庫 ID 或任何東西,但必須是您的使用者物件的主鍵 – 並傳回一個使用者物件或 None

authenticate 方法接受一個 request 引數和憑證作為關鍵字引數。大多數情況下,它會看起來像這樣

from django.contrib.auth.backends import BaseBackend


class MyBackend(BaseBackend):
    def authenticate(self, request, username=None, password=None):
        # Check the username/password and return a user.
        ...

但它也可以驗證權杖,如下所示

from django.contrib.auth.backends import BaseBackend


class MyBackend(BaseBackend):
    def authenticate(self, request, token=None):
        # Check the token and return a user.
        ...

無論哪種方式,authenticate() 都應該檢查它收到的憑證,如果憑證有效,則傳回一個與這些憑證相符的使用者物件。如果憑證無效,則應該傳回 None

requestHttpRequest,如果沒有將其提供給 authenticate() (將其傳遞給後端),則可以是 None

Django 管理員與 Django 使用者物件緊密耦合。例如,若要讓使用者存取管理員,User.is_staffUser.is_active 必須為 True (詳情請參閱 AdminSite.has_permission())。

處理此問題的最佳方法是為每個存在於您的後端的使用者 (例如,在您的 LDAP 目錄、您的外部 SQL 資料庫等中) 建立一個 Django User 物件。您可以編寫指令碼預先執行此操作,或者您的 authenticate 方法可以在使用者第一次登入時執行此操作。

以下是一個範例後端,它會針對在您的 settings.py 檔案中定義的使用者名稱和密碼變數進行驗證,並在使用者第一次驗證時建立 Django User 物件。在此範例中,建立的 Django User 物件是一個超級使用者,他將擁有管理員的完全存取權

from django.conf import settings
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import User


class SettingsBackend(BaseBackend):
    """
    Authenticate against the settings ADMIN_LOGIN and ADMIN_PASSWORD.

    Use the login name and a hash of the password. For example:

    ADMIN_LOGIN = 'admin'
    ADMIN_PASSWORD = 'pbkdf2_sha256$30000$Vo0VlMnkR4Bk$qEvtdyZRWTcOsCnI/oQ7fVOu1XAURIZYoOZ3iq8Dr4M='
    """

    def authenticate(self, request, username=None, password=None):
        login_valid = settings.ADMIN_LOGIN == username
        pwd_valid = check_password(password, settings.ADMIN_PASSWORD)
        if login_valid and pwd_valid:
            try:
                user = User.objects.get(username=username)
            except User.DoesNotExist:
                # Create a new user. There's no need to set a password
                # because only the password from settings.py is checked.
                user = User(username=username)  # is_active defaults to True.
                user.is_staff = True
                user.is_superuser = True
                user.save()
            return user
        return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

處理自訂後端中的授權

自訂驗證後端可以提供他們自己的權限。

使用者模型及其管理器會將權限查找函數 (get_user_permissions()get_group_permissions()get_all_permissions()has_perm()has_module_perms()with_perm()) 委派給任何實作這些函數的驗證後端。

給予使用者的權限將是所有後端傳回的所有權限的超集。也就是說,Django 會將任何一個後端授予的權限授予使用者。

如果後端在 has_perm()has_module_perms() 中引發 PermissionDenied 例外,授權會立即失敗,Django 不會檢查後續的後端。

後端可以為魔法管理員實作權限,如下所示

from django.contrib.auth.backends import BaseBackend


class MagicAdminBackend(BaseBackend):
    def has_perm(self, user_obj, perm, obj=None):
        return user_obj.username == settings.ADMIN_LOGIN

這會給予上述範例中被授權的使用者完整權限。請注意,除了傳遞給相關的 django.contrib.auth.models.User 函數的相同引數之外,後端驗證函數都會接收使用者物件作為引數,此物件可能是匿名使用者。

完整的授權實作可以在 django/contrib/auth/backends.py 中的 ModelBackend 類別中找到,它是預設的後端,並且大多數時候會查詢 auth_permission 表格。

匿名使用者的授權

匿名使用者是指未經驗證的使用者,也就是說他們沒有提供有效的驗證詳細資料。然而,這並不一定表示他們沒有被授權做任何事情。在最基本的層面上,大多數網站會授權匿名使用者瀏覽大部分的網站,並且許多網站允許匿名發佈評論等。

Django 的權限框架沒有地方儲存匿名使用者的權限。但是,傳遞給驗證後端的使用者物件可能是 django.contrib.auth.models.AnonymousUser 物件,允許後端為匿名使用者指定自訂的授權行為。這對於可重複使用的應用程式的作者特別有用,他們可以將所有授權問題委派給驗證後端,而無需設定,例如,來控制匿名存取。

非活躍使用者的授權

非活躍使用者是指其 is_active 欄位設定為 False 的使用者。ModelBackendRemoteUserBackend 驗證後端會禁止這些使用者進行驗證。如果自訂使用者模型沒有 is_active 欄位,則所有使用者都將被允許進行驗證。

如果您希望允許非活躍使用者進行驗證,可以使用 AllowAllUsersModelBackendAllowAllUsersRemoteUserBackend

權限系統中對匿名使用者的支援允許一種情況,即匿名使用者有權限執行某些操作,而非活躍的已驗證使用者則沒有。

不要忘記在您自己的後端權限方法中測試使用者的 is_active 屬性。

處理物件權限

Django 的權限框架具有物件權限的基礎,儘管核心中沒有實作。這表示檢查物件權限將始終返回 False 或一個空列表(取決於執行的檢查)。驗證後端將接收每個物件相關授權方法的關鍵字參數 objuser_obj,並且可以適當地返回物件級別的權限。

自訂權限

要為給定的模型物件建立自訂權限,請使用 permissions 模型 Meta 屬性

Task 模型範例會建立兩個自訂權限,也就是使用者可以使用或不能使用 Task 實例執行的動作,這些動作是您的應用程式特有的

class Task(models.Model):
    ...

    class Meta:
        permissions = [
            ("change_task_status", "Can change the status of tasks"),
            ("close_task", "Can remove a task by setting its status as closed"),
        ]

這唯一的作用是在您執行 manage.py migrate 時建立這些額外的權限(建立權限的函數連接到 post_migrate 信號)。您的程式碼負責在使用者嘗試存取應用程式提供的功能時檢查這些權限的值(變更任務的狀態或關閉任務)。繼續上面的範例,以下檢查使用者是否可以關閉任務

user.has_perm("app.close_task")

擴充現有的 User 模型

有兩種方法可以擴充預設的 User 模型,而無需替換您自己的模型。如果您的變更純粹是行為上的,並且不需要變更資料庫中儲存的內容,則可以建立基於 User代理模型。這允許使用代理模型提供的任何功能,包括預設排序、自訂管理器或自訂模型方法。

如果您希望儲存與 User 相關的資訊,您可以使用 OneToOneField 指向包含額外資訊欄位的模型。此一對一模型通常稱為設定檔模型,因為它可能會儲存有關網站使用者的非驗證相關資訊。例如,您可以建立一個 Employee 模型

from django.contrib.auth.models import User


class Employee(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    department = models.CharField(max_length=100)

假設現有的員工 Fred Smith 同時擁有 User 和 Employee 模型,您可以使用 Django 的標準相關模型慣例來存取相關資訊

>>> u = User.objects.get(username="fsmith")
>>> freds_department = u.employee.department

若要將設定檔模型的欄位新增至管理頁面中的使用者頁面,請在您的應用程式的 admin.py 中定義一個 InlineModelAdmin(在此範例中,我們將使用 StackedInline),並將其新增至使用 User 類別註冊的 UserAdmin 類別

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User

from my_user_profile_app.models import Employee


# Define an inline admin descriptor for Employee model
# which acts a bit like a singleton
class EmployeeInline(admin.StackedInline):
    model = Employee
    can_delete = False
    verbose_name_plural = "employee"


# Define a new User admin
class UserAdmin(BaseUserAdmin):
    inlines = [EmployeeInline]


# Re-register UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)

這些設定檔模型在任何方面都不是特殊的 - 它們只是恰好與使用者模型具有一對一連結的 Django 模型。因此,它們不會在建立使用者時自動建立,但可以使用 django.db.models.signals.post_save 來適當地建立或更新相關模型。

使用相關模型會導致額外的查詢或聯結來擷取相關資料。根據您的需求,包含相關欄位的自訂使用者模型可能是您更好的選擇,但是,您專案應用程式內與預設使用者模型的現有關係可能會使額外的資料庫負載合理化。

替換自訂 User 模型

某些專案可能具有 Django 的內建 User 模型並不總是適用的驗證需求。例如,在某些網站上,使用電子郵件地址作為您的識別令牌,而不是使用者名稱更有意義。

Django 允許您藉由為 AUTH_USER_MODEL 設定提供參考自訂模型的值來覆寫預設使用者模型

AUTH_USER_MODEL = "myapp.MyUser"

此點對描述 Django 應用程式的 label(必須在您的 INSTALLED_APPS 中)和您希望用作使用者模型的 Django 模型名稱。

在開始專案時使用自訂使用者模型

如果您要開始一個新專案,您可以藉由子類別化 AbstractUser 來設定與預設使用者模型行為相同的自訂使用者模型

from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    pass

別忘了將 AUTH_USER_MODEL 指向它。在第一次建立任何遷移或執行 manage.py migrate 之前執行此操作。

此外,在應用程式的 admin.py 中註冊模型

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User

admin.site.register(User, UserAdmin)

在專案中期變更為自訂使用者模型

在您建立資料庫表格後變更 AUTH_USER_MODEL 是可能的,但可能很複雜,因為它會影響外鍵和多對多關係等。

這個變更無法自動完成,需要手動修正您的結構描述、將您的資料從舊的使用者表格移轉,並可能需要手動重新套用一些遷移。請參閱 #25313 以了解步驟概要。

由於 Django 的可替換模型動態依賴功能存在限制,AUTH_USER_MODEL 所參考的模型必須在其應用程式的首次遷移中建立(通常稱為 0001_initial);否則,您將會遇到依賴問題。

此外,當執行遷移時,您可能會遇到 CircularDependencyError,因為 Django 無法由於動態依賴而自動中斷依賴迴圈。如果您看到此錯誤,您應該將您的使用者模型所依賴的模型移至第二個遷移,以中斷迴圈。(您可以嘗試建立兩個彼此之間具有 ForeignKey 的普通模型,並查看 makemigrations 如何解決該循環依賴,如果您想了解通常如何完成。)

可重複使用的應用程式和 AUTH_USER_MODEL

可重複使用的應用程式不應實作自訂使用者模型。一個專案可能會使用許多應用程式,而兩個實作自訂使用者模型的可重複使用應用程式無法同時使用。如果需要在您的應用程式中儲存每個使用者的資訊,請使用 ForeignKeyOneToOneField 連接到 settings.AUTH_USER_MODEL,如下所述。

參照 User 模型

如果您直接參照 User(例如,在外部鍵中參照它),您的程式碼將無法在 AUTH_USER_MODEL 設定已變更為不同使用者模型的專案中運作。

get_user_model()[原始碼]

您應該使用 django.contrib.auth.get_user_model() 來參照使用者模型,而不是直接參照 User。此方法將傳回目前作用中的使用者模型 – 如果有指定自訂使用者模型,則為自訂使用者模型,否則為 User

當您定義使用者模型的外來鍵或多對多關係時,您應該使用 AUTH_USER_MODEL 設定來指定自訂模型。例如

from django.conf import settings
from django.db import models


class Article(models.Model):
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )

當您連接到使用者模型傳送的訊號時,您應該使用 AUTH_USER_MODEL 設定來指定自訂模型。例如

from django.conf import settings
from django.db.models.signals import post_save


def post_save_receiver(sender, instance, created, **kwargs):
    pass


post_save.connect(post_save_receiver, sender=settings.AUTH_USER_MODEL)

一般而言,在匯入時執行的程式碼中使用 AUTH_USER_MODEL 設定來參照使用者模型最容易,但是,也可以在 Django 匯入模型的同時呼叫 get_user_model(),因此您可以使用 models.ForeignKey(get_user_model(), ...)

如果您的應用程式使用多個使用者模型進行測試,例如使用 @override_settings(AUTH_USER_MODEL=...),並且您在模組級變數中快取 get_user_model() 的結果,則可能需要監聽 setting_changed 訊號來清除快取。例如

from django.apps import apps
from django.contrib.auth import get_user_model
from django.core.signals import setting_changed
from django.dispatch import receiver


@receiver(setting_changed)
def user_model_swapped(*, setting, **kwargs):
    if setting == "AUTH_USER_MODEL":
        apps.clear_cache()
        from myapp import some_module

        some_module.UserModel = get_user_model()

指定自訂使用者模型

當您開始使用自訂使用者模型建立專案時,請停下來思考這是否是您專案的正確選擇。

將所有與使用者相關的資訊保留在一個模型中,可以免除檢索相關模型時需要額外的或更複雜的資料庫查詢。另一方面,將應用程式特定的使用者資訊儲存在與您的自訂使用者模型具有關係的模型中可能更合適。這允許每個應用程式指定自己的使用者資料需求,而不會與其他應用程式的假設發生衝突或破壞。這也表示您會盡可能讓您的使用者模型保持簡單,專注於驗證,並遵循 Django 期望自訂使用者模型滿足的最低要求。

如果您使用預設的驗證後端,則您的模型必須具有一個可用於識別目的的唯一欄位。這可以是使用者名稱、電子郵件地址或任何其他唯一屬性。如果您使用可以支援非唯一使用者名稱的自訂驗證後端,則允許使用非唯一的使用者名稱欄位。

建立相容的自訂使用者模型最簡單的方法是繼承自 AbstractBaseUserAbstractBaseUser 提供了使用者模型的核心實作,包括雜湊密碼和權杖化密碼重設。然後,您必須提供一些關鍵實作詳細資訊

class models.CustomUser
USERNAME_FIELD

一個字串,描述使用者模型上用作唯一識別碼的欄位名稱。這通常會是某種類型的使用者名稱,但也可以是電子郵件地址或任何其他唯一識別碼。除非您使用可以支援非唯一使用者名稱的自訂驗證後端,否則該欄位必須是唯一的(例如,在其定義中設定 unique=True)。

在以下範例中,欄位 identifier 用作識別欄位

class MyUser(AbstractBaseUser):
    identifier = models.CharField(max_length=40, unique=True)
    ...
    USERNAME_FIELD = "identifier"
EMAIL_FIELD

一個字串,描述 User 模型上的電子郵件欄位名稱。此值由 get_email_field_name() 傳回。

REQUIRED_FIELDS

當透過 createsuperuser 管理命令建立使用者時,將提示輸入的欄位名稱清單。系統會提示使用者為每個欄位提供一個值。它必須包含 blankFalse 或未定義的任何欄位,並且可以包含您想要在互動式建立使用者時提示的其他欄位。REQUIRED_FIELDS 在 Django 的其他部分(例如在管理介面中建立使用者)沒有任何作用。

例如,這是定義兩個必要欄位(出生日期和身高)的使用者模型的部分定義

class MyUser(AbstractBaseUser):
    ...
    date_of_birth = models.DateField()
    height = models.FloatField()
    ...
    REQUIRED_FIELDS = ["date_of_birth", "height"]

注意

REQUIRED_FIELDS 必須包含使用者模型上的所有必要欄位,但不應包含 USERNAME_FIELDpassword,因為這些欄位始終會提示輸入。

is_active

一個布林值屬性,指示使用者是否被視為「活動」。此屬性作為 AbstractBaseUser 的屬性提供,預設值為 True。您選擇如何實作它將取決於您選擇的驗證後端的詳細資訊。請參閱 內建使用者模型上的 is_active 屬性 的說明文件以了解詳細資訊。

get_full_name()

選用。使用者更正式的識別碼,例如他們的全名。如果實作,這會與 django.contrib.admin 中物件的歷史記錄中的使用者名稱一起顯示。

get_short_name()

選用。使用者簡短、非正式的識別名稱,例如他們的名字。如果實作此方法,它將會取代 django.contrib.admin 標頭中對使用者的問候語中的使用者名稱。

匯入 AbstractBaseUser

AbstractBaseUserBaseUserManager 可以從 django.contrib.auth.base_user 匯入,因此可以在不將 django.contrib.auth 包含在 INSTALLED_APPS 中也能匯入。

下列屬性和方法在 AbstractBaseUser 的任何子類別上皆可使用

class models.AbstractBaseUser
get_username()

傳回由 USERNAME_FIELD 指定的欄位值。

clean()

透過呼叫 normalize_username() 來正規化使用者名稱。如果您覆寫此方法,請務必呼叫 super() 以保留正規化。

classmethod get_email_field_name()

傳回由 EMAIL_FIELD 屬性指定的電子郵件欄位名稱。如果未指定 EMAIL_FIELD,則預設為 'email'

classmethod normalize_username(username)

對使用者名稱套用 NFKC Unicode 正規化,使外觀相同但 Unicode 碼位不同的字元被視為相同。

is_authenticated

唯讀屬性,始終為 True(與始終為 FalseAnonymousUser.is_authenticated 相反)。這是一種判斷使用者是否已通過驗證的方式。這並不意味著任何權限,也不會檢查使用者是否處於活動狀態或是否具有有效的會期。即使通常您會在 request.user 上檢查此屬性,以判斷它是否已由 AuthenticationMiddleware 填入(表示目前已登入的使用者),您應該知道此屬性對於任何 User 實例皆為 True

is_anonymous

唯讀屬性,始終為 False。這是一種區分 UserAnonymousUser 物件的方法。一般來說,您應該優先使用 is_authenticated 而非此屬性。

set_password(raw_password)

將使用者的密碼設定為給定的原始字串,並處理密碼雜湊。不會儲存 AbstractBaseUser 物件。

當 raw_password 為 None 時,密碼將會被設定為無法使用的密碼,如同使用 set_unusable_password() 一樣。

check_password(raw_password)
acheck_password(raw_password)

非同步版本: acheck_password()

如果給定的原始字串是使用者的正確密碼,則傳回 True。(這會處理密碼雜湊以進行比較。)

在 Django 5.0 中變更

新增了 acheck_password() 方法。

set_unusable_password()

將使用者標記為未設定密碼。這與密碼為空白字串不同。此使用者的 check_password() 永遠不會傳回 True。不會儲存 AbstractBaseUser 物件。

如果您的應用程式的驗證是針對現有的外部來源(例如 LDAP 目錄)進行,您可能需要此方法。

has_usable_password()

如果已對此使用者呼叫 set_unusable_password(),則傳回 False

get_session_auth_hash()

傳回密碼欄位的 HMAC。用於 密碼變更時的會期失效

get_session_auth_fallback_hash()

使用 SECRET_KEY_FALLBACKS 產生密碼欄位的 HMAC。由 get_user() 使用。

AbstractUser 繼承自 AbstractBaseUser

class models.AbstractUser
clean()

透過呼叫 BaseUserManager.normalize_email() 來正規化電子郵件。如果您覆寫此方法,請務必呼叫 super() 以保留正規化。

為自訂使用者模型撰寫管理器

您也應該為您的使用者模型定義一個自訂的管理員。如果您的使用者模型定義了與 Django 預設使用者相同的 usernameemailis_staffis_activeis_superuserlast_logindate_joined 欄位,您可以安裝 Django 的 UserManager;但是,如果您的使用者模型定義了不同的欄位,您需要定義一個繼承自 BaseUserManager 的自訂管理員,並提供兩個額外的方法

class models.CustomUserManager
create_user(username_field, password=None, **other_fields)

create_user() 的原型應該接受使用者名稱欄位,以及所有必要的欄位作為引數。例如,如果您的使用者模型使用 email 作為使用者名稱欄位,並且有 date_of_birth 作為必要的欄位,那麼 create_user 應該定義為

def create_user(self, email, date_of_birth, password=None):
    # create user here
    ...
create_superuser(username_field, password=None, **other_fields)

create_superuser() 的原型應該接受使用者名稱欄位,以及所有必要的欄位作為引數。例如,如果您的使用者模型使用 email 作為使用者名稱欄位,並且有 date_of_birth 作為必要的欄位,那麼 create_superuser 應該定義為

def create_superuser(self, email, date_of_birth, password=None):
    # create superuser here
    ...

對於 ForeignKeyUSERNAME_FIELDREQUIRED_FIELDS 中,這些方法會接收 to_field(預設為 primary_key)的值,該值為現有實例的值。

BaseUserManager 提供以下實用方法

class models.BaseUserManager
classmethod normalize_email(email)

透過將電子郵件地址的網域部分轉換為小寫來標準化電子郵件地址。

get_by_natural_key(username)

使用 USERNAME_FIELD 指定的欄位內容來檢索使用者實例。

擴充 Django 的預設 User

如果您對 Django 的 User 模型完全滿意,但您想新增一些額外的個人資料資訊,您可以子類化 django.contrib.auth.models.AbstractUser 並新增您的自訂個人資料欄位,儘管我們建議使用單獨的模型,如 指定自訂使用者模型 中所述。AbstractUser 作為一個 抽象模型,提供了預設 User 的完整實作。

自訂使用者和內建驗證表單

Django 的內建 表單視圖 對他們正在使用的使用者模型做了一些假設。

以下表單與 AbstractBaseUser 的任何子類別相容

如果符合這些假設,則以下表單對使用者模型進行假設,並且可以直接使用

  • PasswordResetForm:假設使用者模型有一個欄位,用於儲存使用者的電子郵件地址,該名稱由 get_email_field_name()(預設為 email)傳回,可用於識別使用者,並且有一個名為 is_active 的布林值欄位,以防止為非活動使用者重設密碼。

最後,以下表單與 User 綁定,需要重寫或擴充才能使用自訂使用者模型

如果您的自訂使用者模型是 AbstractUser 的子類別,那麼您可以透過這種方式擴充這些表單

from django.contrib.auth.forms import UserCreationForm
from myapp.models import CustomUser


class CustomUserCreationForm(UserCreationForm):
    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = UserCreationForm.Meta.fields + ("custom_field",)

自訂使用者和 django.contrib.admin

如果您希望您的自訂使用者模型也能與管理員一起使用,您的使用者模型必須定義一些額外的屬性和方法。這些方法允許管理員控制使用者對管理員內容的存取權限

class models.CustomUser
is_staff

如果允許使用者存取管理網站,則傳回 True

is_active

如果使用者帳戶目前處於活動狀態,則傳回 True

has_perm(perm, obj=None):

如果使用者具有指定的權限,則傳回 True。如果提供了 obj,則需要針對特定物件實例檢查權限。

has_module_perms(app_label)

如果使用者具有存取給定應用程式中模型的權限,則傳回 True

您還需要向管理員註冊您的自訂使用者模型。如果您的自訂使用者模型擴充了 django.contrib.auth.models.AbstractUser,您可以使用 Django 現有的 django.contrib.auth.admin.UserAdmin 類別。但是,如果您的使用者模型擴充了 AbstractBaseUser,您需要定義一個自訂的 ModelAdmin 類別。可能可以子類化預設的 django.contrib.auth.admin.UserAdmin;但是,您需要覆寫任何引用不在您的自訂使用者類別上的 django.contrib.auth.models.AbstractUser 上的欄位的定義。

注意

如果您使用的是自訂的 ModelAdmin,它是 django.contrib.auth.admin.UserAdmin 的子類別,那麼您需要將您的自訂欄位新增到 fieldsets(用於編輯使用者時使用的欄位)和 add_fieldsets(用於建立使用者時使用的欄位)。例如

from django.contrib.auth.admin import UserAdmin


class CustomUserAdmin(UserAdmin):
    ...
    fieldsets = UserAdmin.fieldsets + ((None, {"fields": ["custom_field"]}),)
    add_fieldsets = UserAdmin.add_fieldsets + ((None, {"fields": ["custom_field"]}),)

有關更多詳細資訊,請參閱 完整範例

自訂使用者和權限

為了方便將 Django 的權限框架包含到您自己的使用者類別中,Django 提供了 PermissionsMixin。這是一個抽象模型,您可以將其包含在使用者模型的類別階層中,為您提供支援 Django 權限模型的所有方法和資料庫欄位。

PermissionsMixin 提供以下方法和屬性

class models.PermissionsMixin
is_superuser

布林值。指定此使用者擁有所有權限,而無需明確指派。

get_user_permissions(obj=None)

返回使用者直接擁有的權限字串集合。

如果傳入 obj,則僅返回此特定物件的使用者權限。

get_group_permissions(obj=None)

返回使用者透過其群組擁有的權限字串集合。

如果傳入 obj,則僅返回此特定物件的群組權限。

get_all_permissions(obj=None)

返回使用者擁有的權限字串集合,包括群組權限和使用者權限。

如果傳入 obj,則僅返回此特定物件的權限。

has_perm(perm, obj=None)

如果使用者擁有指定的權限,則返回 True,其中 perm 的格式為 "<應用程式 標籤>.<權限 代碼名稱>" (請參閱 權限)。如果 User.is_activeis_superuser 皆為 True,則此方法始終返回 True

如果傳入 obj,此方法將不會檢查模型的權限,而是檢查此特定物件的權限。

has_perms(perm_list, obj=None)

如果使用者擁有指定的每個權限,則返回 True,其中每個權限的格式為 "<應用程式 標籤>.<權限 代碼名稱>"。如果 User.is_activeis_superuser 皆為 True,則此方法始終返回 True

如果傳入 obj,此方法將不會檢查模型的權限,而是檢查此特定物件的權限。

has_module_perms(package_name)

如果使用者在給定的套件(Django 應用程式標籤)中擁有任何權限,則返回 True。如果 User.is_activeis_superuser 皆為 True,則此方法始終返回 True

PermissionsMixinModelBackend

如果您不包含 PermissionsMixin,則必須確保您不會在 ModelBackend 上調用權限方法。ModelBackend 假設您的使用者模型上提供了某些欄位。如果您的使用者模型未提供這些欄位,則在您檢查權限時會收到資料庫錯誤。

自訂使用者和 Proxy 模型

自訂使用者模型的一個限制是,安裝自訂使用者模型會破壞任何擴展 User 的 Proxy 模型。Proxy 模型必須基於具體的基底類別;透過定義自訂使用者模型,您移除了 Django 可靠識別基底類別的能力。

如果您的專案使用 Proxy 模型,您必須修改 Proxy 以擴展專案中使用的使用者模型,或者將 Proxy 的行為合併到您的 User 子類別中。

完整範例

以下是符合管理員規範的自訂使用者應用程式範例。此使用者模型使用電子郵件地址作為使用者名稱,並具有必要的出生日期;除了使用者帳戶上的 admin 標記外,它不提供任何權限檢查。此模型將與所有內建的身份驗證表單和視圖相容,除了使用者建立表單。此範例說明了大多數元件如何協同工作,但不打算直接複製到生產環境的專案中使用。

此程式碼將全部存在於自訂驗證應用程式的 models.py 檔案中

from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser


class MyUserManager(BaseUserManager):
    def create_user(self, email, date_of_birth, password=None):
        """
        Creates and saves a User with the given email, date of
        birth and password.
        """
        if not email:
            raise ValueError("Users must have an email address")

        user = self.model(
            email=self.normalize_email(email),
            date_of_birth=date_of_birth,
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, date_of_birth, password=None):
        """
        Creates and saves a superuser with the given email, date of
        birth and password.
        """
        user = self.create_user(
            email,
            password=password,
            date_of_birth=date_of_birth,
        )
        user.is_admin = True
        user.save(using=self._db)
        return user


class MyUser(AbstractBaseUser):
    email = models.EmailField(
        verbose_name="email address",
        max_length=255,
        unique=True,
    )
    date_of_birth = models.DateField()
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)

    objects = MyUserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = ["date_of_birth"]

    def __str__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        "Does the user have a specific permission?"
        # Simplest possible answer: Yes, always
        return True

    def has_module_perms(self, app_label):
        "Does the user have permissions to view the app `app_label`?"
        # Simplest possible answer: Yes, always
        return True

    @property
    def is_staff(self):
        "Is the user a member of staff?"
        # Simplest possible answer: All admins are staff
        return self.is_admin

然後,要將此自訂使用者模型註冊到 Django 的管理員介面,需要在應用程式的 admin.py 檔案中加入以下程式碼

from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError

from customauth.models import MyUser


class UserCreationForm(forms.ModelForm):
    """A form for creating new users. Includes all the required
    fields, plus a repeated password."""

    password1 = forms.CharField(label="Password", widget=forms.PasswordInput)
    password2 = forms.CharField(
        label="Password confirmation", widget=forms.PasswordInput
    )

    class Meta:
        model = MyUser
        fields = ["email", "date_of_birth"]

    def clean_password2(self):
        # Check that the two password entries match
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise ValidationError("Passwords don't match")
        return password2

    def save(self, commit=True):
        # Save the provided password in hashed format
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user


class UserChangeForm(forms.ModelForm):
    """A form for updating users. Includes all the fields on
    the user, but replaces the password field with admin's
    disabled password hash display field.
    """

    password = ReadOnlyPasswordHashField()

    class Meta:
        model = MyUser
        fields = ["email", "password", "date_of_birth", "is_active", "is_admin"]


class UserAdmin(BaseUserAdmin):
    # The forms to add and change user instances
    form = UserChangeForm
    add_form = UserCreationForm

    # The fields to be used in displaying the User model.
    # These override the definitions on the base UserAdmin
    # that reference specific fields on auth.User.
    list_display = ["email", "date_of_birth", "is_admin"]
    list_filter = ["is_admin"]
    fieldsets = [
        (None, {"fields": ["email", "password"]}),
        ("Personal info", {"fields": ["date_of_birth"]}),
        ("Permissions", {"fields": ["is_admin"]}),
    ]
    # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
    # overrides get_fieldsets to use this attribute when creating a user.
    add_fieldsets = [
        (
            None,
            {
                "classes": ["wide"],
                "fields": ["email", "date_of_birth", "password1", "password2"],
            },
        ),
    ]
    search_fields = ["email"]
    ordering = ["email"]
    filter_horizontal = []


# Now register the new UserAdmin...
admin.site.register(MyUser, UserAdmin)
# ... and, since we're not using Django's built-in permissions,
# unregister the Group model from admin.
admin.site.unregister(Group)

最後,使用 settings.py 中的 AUTH_USER_MODEL 設定,指定自訂模型作為專案的預設使用者模型

AUTH_USER_MODEL = "customauth.MyUser"
返回頂部