Django內建的使用者驗證系統十分強大。大多數情況下,它可以拿來就用,能幫我們省去很多開發、測試的工作。它能滿足大多數的使用情況並且很安全。但是有時候,為滿足我們的網路應用需求,需要對它進行一些微調。
一般來說,我們希望更多地儲存與使用者有關的資料。如果你的網路應用具有社交屬性,你可能希望儲存使用者簡介、地理位置以及其他相關的東西。
在此教程裡,我將簡單呈現擴充套件Django使用者模型的方法,而你並不需要從頭開始去實現每一個細節。
擴充套件現有使用者模型的方法
一般來說,有四種不同的方法可以擴充套件現有使用者模型。下面介紹這四種方法使用的場景。
方法1:使用代理模型
什麼是代理模型?
它是一種模型繼承,這種模型在資料庫中無需建立新表格。它一般被用於改變現有模型的行為方式(例如預設的命令,增加新方法等等),而不影響現有資料庫的架構。
什麼時候需要用代理模型?
當不需要在資料庫中儲存額外的資訊而僅僅是簡單的增加操作方法或更改模型的查詢管理方式時,就需要使用代理模型來擴充套件現有使用者模型。
方法2:使用和使用者模型一對一的連結(使用Profile擴充套件User模組)
什麼是一對一的連結?
標準的Django模型都會有自己的資料庫表,並通過OneToOneField與現有使用者模型形成一對一的關聯。
什麼時候使用一對一連結?
當要儲存與現有使用者模型相關而又與驗證過程無關的額外的資訊時,就需要使用一對一連結。我們一般稱之為使用者配置(User Profile)。
方法3:擴充套件AbstractBaseUser建立自定義使用者模型
什麼是基於擴充套件AbstractBaseUser來建立的自定義使用者模型?
它是繼承於AbstractBaseUser類的一個全新的使用者系統。這需要通過settings.py進行特別的維護和更新一些相關的檔案。理想的情況是,它在專案開始時就已經被設計,因為它對資料庫架構影響很大。在執行時都需要額外的維護。
什麼時候需要擴充套件AbstractBaseUser類自定義使用者模型呢?
當應用對驗證過程具有特殊要求時,那麼需要自定義一個使用者模型。比如,在一些將郵件地址代替使用者名稱作為驗證令牌的情況時,自定義使用者模型更合適。
方法4:擴充套件AbstractUser來建立自定義使用者模型
什麼是基於擴充套件AbstractUser建立的自定義使用者模型?
它是繼承於AbstractUser類的一個全新的使用者系統。它也需要通過settings.py進行特別的維護和更新一些相關的檔案。理想的情況是,它在專案開始之前就已經被設計,因為它對資料庫架構影響很大。在執行時需要額外的維護。
什麼時候需要擴充套件AbstractBaseUser類的自定義使用者模型呢?
當你對Django處理身份驗證過程很滿意而又不會改動它時,那可以使用它。或者,你想直接在使用者模型中增加一些額外的資訊而不想建立新的類時,也可以使用它。(像方法2)
使用代理模式來擴充使用者模型
這種方法對現有使用者模型影響最小而且不會帶來任何新的缺陷。但是它在很多方面實現受到限制。
下面是實現它的方法:
1 2 3 4 5 6 7 8 9 10 11 12 |
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): ... |
在上面的例子裡,我們定義了一個Person代理模型。通過在內部Meta類中新增“proxy=true”屬性,我們告訴Django這是個代理模型。
在這個例子中,我已經定義了預設的命令,為模組分配了自定義Manager,而且定義了新方法do_something。
分別使用User.objects.all()和Person.objects.all()查詢相同的資料庫表並沒有什麼意義,因為它們唯一的不同在於我們為代理模型定義的行為方式。
使用一對一連結擴充套件使用者模型(使用Profile)
很有可能就是你所需要的方法。對我個人來說,這是我使用的最多的方法。我們將會建立一個新的Django模組去儲存和使用者模型相關的額外資訊。
記住使用這種方法會額外增加對相關資訊的查詢和檢索。基本上當進入相關資料時,Django就會開啟一個額外的查詢。但是在大多數情況下,這可以避免。我將會在後面對此進行說明。
一般,我會將Django模組命名為Profile
1 2 3 4 5 6 7 8 |
from django.db import models from django.contrib.auth.models import User class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) bio = models.TextField(max_length=500, blank=True) location = models.CharField(max_length=30, blank=True) birth_date = models.DateField(null=True, blank=True) |
這就是這個方法妙處:我們再定義一些訊號就能使得:當我們建立和更新使用者例項時,Profile模組也會被自動建立和更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from django.db import models from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) bio = models.TextField(max_length=500, blank=True) location = models.CharField(max_length=30, blank=True) birth_date = models.DateField(null=True, blank=True) @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: Profile.objects.create(user=instance) @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs): instance.profile.save() |
基本上無論什麼時候儲存事件(Save事件)發生,create_user_profile和save_user_profile方法都會被訊號連線到使用者模型。這種訊號被稱為post_save.
那如何使用它?
簡單!在Django模板中測試下面例項:
1 2 3 4 5 6 |
<h2>{{ user.get_full_name }}</h2> <ul> <li>Username: {{ user.username }}</li> <li>Location: {{ user.profile.location }}</li> <li>Birth Date: {{ user.profile.birth_date }}</li> </ul> |
深入看看實現方法?
1 2 3 4 |
def update_profile(request, user_id): user = User.objects.get(pk=user_id) user.profile.bio = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit...' user.save() |
通常來說,永遠不會觸發Profile的save方法。所有的工作都通過使用者模型完成。
如果我正在使用Django的表格呢?
你不知道其實可以一次同時進入多個表格麼?測試一下下面的程式片段:
forms.py
1 2 3 4 5 6 7 8 9 |
class UserForm(forms.ModelForm): class Meta: model = User fields = ('first_name', 'last_name', 'email') class ProfileForm(forms.ModelForm): class Meta: model = Profile fields = ('url', 'location', 'company') |
views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@login_required @transaction.atomic def update_profile(request): if request.method == 'POST': user_form = UserForm(request.POST, instance=request.user) profile_form = ProfileForm(request.POST, instance=request.user.profile) if user_form.is_valid() and profile_form.is_valid(): user_form.save() profile_form.save() messages.success(request, _('Your profile was successfully updated!')) return redirect('settings:profile') else: messages.error(request, _('Please correct the error below.')) else: user_form = UserForm(instance=request.user) profile_form = ProfileForm(instance=request.user.profile) return render(request, 'profiles/profile.html', { 'user_form': user_form, 'profile_form': profile_form }) |
profile.html
1 2 3 4 5 6 |
<form method="post"> {% csrf_token %} {{ user_form.as_p }} {{ profile_form.as_p }} <button type="submit">Save changes</button> </form> |
那增加的資料庫查詢的問題呢?
我已經在“Optimeze Database Queries(優化資料庫查詢)”的帖子裡已經解釋了這個問題。你可以點這瞭解。
但在這長話短說解釋一下:Django關聯為淺關聯。也就是說當進入一個相關的屬性時,Django所做的工作也只是查詢資料庫而已。有時候這會造成一些意想不到的後果,比如像開啟成千上萬的查詢程式。這個問題可以使用select_related方法緩和一些。
當預知將會進入相關的資料時,可以使用簡單的查詢先將它取出來。
1 |
users = User.objects.all().select_related('profile') |
使用擴充套件AbstractBaseUser的自定義模組擴充套件使用者模型
這是個比較難理解的方法。實話說,我儘量避免使用這種方法。但是有時候你無法繞過它。並且這種方法可以實現的很完美。幾乎沒有像這樣既可以是最完美也可以是最糟糕的解決方案了。大多數情況下或多或少都有合適的解決方案。但如果這種方案是你問題的最好解決方案,那就使用這種方案吧。
這種方法,我曾經不得不使用過一次。老實說,我不知道這是否是最簡潔的實現方法,反正也不管那麼多了:
我需要用郵件地址作為身份驗證令牌,整個方案裡username(使用者名稱)對我來說毫無用處。而且也沒有必要使用is_staff標記,因為我沒有使用Django Admin。
下面是我自己定義的使用者模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
from __future__ import unicode_literals from django.db import models from django.contrib.auth.models import PermissionsMixin from django.contrib.auth.base_user import AbstractBaseUser from django.utils.translation import ugettext_lazy as _ from .managers import UserManager class User(AbstractBaseUser, PermissionsMixin): email = models.EmailField(_('email address'), unique=True) first_name = models.CharField(_('first name'), max_length=30, blank=True) last_name = models.CharField(_('last name'), max_length=30, blank=True) date_joined = models.DateTimeField(_('date joined'), auto_now_add=True) is_active = models.BooleanField(_('active'), default=True) avatar = models.ImageField(upload_to='avatars/', null=True, blank=True) objects = UserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] class Meta: verbose_name = _('user') verbose_name_plural = _('users') def get_full_name(self): ''' Returns the first_name plus the last_name, with a space in between. ''' full_name = '%s %s' % (self.first_name, self.last_name) return full_name.strip() def get_short_name(self): ''' Returns the short name for the user. ''' return self.first_name def email_user(self, subject, message, from_email=None, **kwargs): ''' Sends an email to this User. ''' send_mail(subject, message, from_email, [self.email], **kwargs) |
我希望儘可能保持它與現有使用者模型相近。因為模組繼承於AbstractBaseUser,我們需要遵循下列規則:
USERNAME_FIELD:描述使用者模型欄位名的字串,被用於作為唯一的識別符。欄位必須是唯一的(比如,在它的定義裡設定 unique=True);
REQUIRED_FIELDS:欄位名稱列表,當使用createsuperuser管理命令建立使用者時將會提示這些欄位名。
is_active:標明使用者是否為“active”的布林型別屬性。
get_full_name():正式的長使用者識別符號。通用的解釋是使用者的完整命名,但它可以是用來驗證使用者的任意字串。
get_short_name():簡短非正式的使用者識別標識。通用的解釋是使用者的名(first_name)。
好了,讓我們繼續。我還必須定義我自己的UserManager。儘管現有的使用者管理已經定義了ceate_user和create_superuser方法。
所以,我的UserManager定義如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
from django.contrib.auth.base_user import BaseUserManager class UserManager(BaseUserManager): use_in_migrations = True def _create_user(self, email, password, **extra_fields): """ Creates and saves a User with the given email and password. """ if not email: raise ValueError('The given email 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_user(self, email, password=None, **extra_fields): extra_fields.setdefault('is_superuser', False) return self._create_user(email, password, **extra_fields) def create_superuser(self, email, password, **extra_fields): extra_fields.setdefault('is_superuser', True) if extra_fields.get('is_superuser') is not True: raise ValueError('Superuser must have is_superuser=True.') return self._create_user(email, password, **extra_fields) |
基本上我已經完成了現有UserManager的系列工作,並刪除了username和is_staff屬性。
現在最後一步,我們必須上傳setting.py。再明確AUTH_USER_MODEL屬性。
1 |
AUTH_USER_MODEL=‘core.User’ |
這樣我們就告訴了Django使用我們自定義的模組去替代原來預設的模組。在上面的例子中,我已經在名為core的應用內建立了自定義模組。
那麼如何引用這個模組呢?
有兩種方法。看看Course模組的例子:
1 2 3 4 5 6 7 |
from django.db import models from testapp.core.models import User class Course(models.Model): slug = models.SlugField(max_length=100) name = models.CharField(max_length=100) tutor = models.ForeignKey(User, on_delete=models.CASCADE) |
這樣定義完全可以。但是如果你正在設計一個可重用的應用,並希望它公開可用,那強烈建議使用下面的策略:
1 2 3 4 5 6 7 |
from django.db import models from django.conf import settings class Course(models.Model): slug = models.SlugField(max_length=100) name = models.CharField(max_length=100) tutor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) |
通過擴充套件AbstractUser的自定義模組來擴充套件使用者模型
這種方法相當的直接,因為django.contrib.auth.models.AbstractUser類提供了把預設使用者作為抽象模型的整套的實現方案。
1 2 3 4 5 6 7 |
from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): bio = models.TextField(max_length=500, blank=True) location = models.CharField(max_length=30, blank=True) birth_date = models.DateField(null=True, blank=True) |
然後必須更新settings.py,定義AUTH_USER_MODEL屬性。
1 |
AUTH_USER_MODEL=‘core.User’ |
和前一個方案相似,這種方案的需要在專案開始時就考慮完成並需要額外的維護。它將會改變整個資料庫的架構。如果建立外來鍵匯入使用者模型設定 (from django.conf import settings)而且把引用settings.AUTH_USER_MODEL代替直接引用自定義的使用者模型會更好。
結論
好了。我們已閱讀了四種擴充套件現有使用者模型的不同方法。我已經儘可能將它們說的詳細了。就如我前面所說,沒有最好的解決方案。選擇什麼方案取決於你的需要。保持簡單,並作出明智的選擇。
Proxy Model:對Django User提供的所有實現都很滿意而且不需要儲存額外的資訊。
User Profile:對Django處理名字認證的方式很滿意,而且需要為User增加一些非認證相關的屬性。
Custom User Model from AbstractBaseUser:Django處理使用者驗證的方式不符合你的專案。
Custom User Model from AbstractUser:Django處理使用者驗證的方式很符合你的專案但是你希望在不建立單獨模組的情況下增加一些額外的屬性。
不要猶豫去問我問題,告訴我你對這篇文章的看法。
你也可以加入我的郵件列表。