Django認證系統並不雞肋反而很重要
在使用django-admin startproject
創建項目後,Django就默認安裝了一個採用session實現的認證系統。這是Django相比於其他框架的一大特點:自帶認證系統,開箱即用。有人說它方便,有人說它雞肋,但它作為Django的重要組成部分,學習它有助於我們理解Django框架的核心技術。
安裝
Django默認已安裝,可以在settings.py
中的INSTALLED_APPS
看到:
- django.contrib.auth:認證系統內核,以及默認models等。
- django.contrib.contenttypes:用於關聯許可權和models,從而賦予models的添加/刪除等許可權。
contrib翻譯為普通發布版。
在MIDDLEWARE
可以看到:
SessionMiddleware
:session中間件。AuthenticationMiddleware
:認證中間件。
使用python manage.py migrate
後,資料庫會新增認證系統的這些表:

認證與授權
認證的英文是authentication,授權的英文是authorization。單詞不一樣,咋看有點像。認證是指驗證用戶是誰。授權是指授予已認證用戶許可權。由於認證授權在某種程式上是耦合的,所以Django把它們統稱為「認證」。
認證系統概覽
認證系統的組成部分如下:
- 用戶
- 許可權
- 組
- 密碼管理
- 登錄相關表單(前後端分離不需要)和視圖(接受Web請求並且返回Web響應)
Django框架是MTV模式,類似於MVC模式。Django的View對應MVC的Controller。
- 可配置的backend
以上是Django自帶內容,如果需要更多功能,可以安裝第三方包:
- 密碼增強校驗
- 登錄限流
- OAuth
- 對象級許可權(django-guardian)
以Article舉例,Django是模型級許可權,用戶只能具有全部文章的許可權。django-guardian提供了對象級許可權,可以對單篇文章進行授權。
models.User
User模型是Django認證系統的核心,它的主要屬性包括:
- id
- username
- password
- is_active
- is_superuser
- last_login
- date_joined
django.contrib.auth.models,在django.db.models之上封裝了AbstractBaseUser、AbstractUser、User等模型。
創建超級管理員
cmd中使用createsuperuser
命令:
$ python manage.py createsuperuser
根據提示輸入username、email、password後,就會在資料庫中創建1條超管用戶。
創建用戶
方法1 程式碼創建
在程式碼中使用create_user()
函數來創建用戶:
>>> from django.contrib.auth.models import User
# 創建用戶並保存到資料庫
>>> user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword')
# 修改其他欄位值
>>> user.last_name = 'Lennon'
>>> user.save()
方法2 管理後台創建
訪問//127.0.0.1:8000/admin/
,用超管登錄後,在介面上創建:

修改密碼
方法1 命令行修改
python manage.py changepassword username
根據提示輸入舊密碼、新密碼、確認密碼即可。
方法2 程式碼中修改
>>> from django.contrib.auth.models import User
>>> u = User.objects.get(username='john')
>>> u.set_password('new password')
>>> u.save()
方法3 管理後台修改


用戶認證
框架底層使用authenticate()
函數對用戶進行認證:
authenticate(request=None, **credentials)
credentials是用戶憑證,如用戶名、密碼。
示例:
from django.contrib.auth import authenticate
user = authenticate(username='john', password='secret')
if user is not None:
# A backend authenticated the credentials
else:
# No backend authenticated the credentials
如果認證成功,authenticate()
會返回User。如果用戶憑證無效或者許可權不足,認證後端拋出了PermissionDenied,authenticate()
會返回None。
認證後端
認證後端(authentication backends)是Django做用戶驗證的後端模組,默認為['django.contrib.auth.backends.ModelBackend']
,只會簡單比較請求的用戶名密碼和資料庫中的用戶名密碼是否匹配。可以切換成其他認證後端,也可以重寫authenticate()
進行自定義。我點開了源碼,發現除了Django的認證後端,DRF已經封裝了Session、Token、JWT的認證:

許可權管理
許可權一般分為add、change、delete、view,也就是增刪改查。
默認許可權
Django會在python manage.py migrate
的時候,為每個model創建4種許可權:add、change、delete、view。
比如有個app叫做foo,它有個model叫做Bar,可以使用has_perm()
函數來檢查許可權:
- add:user.has_perm(‘foo.add_bar’)
- change:user.has_perm(‘foo.change_bar’)
- delete:user.has_perm(‘foo.delete_bar’)
- view:user.has_perm(‘foo.view_bar’)
創建新許可權
除了增刪改查許可權,有時我們需要更多的許可權,例如,為myapp中的BlogPost創建一個can_publish許可權:
方法1 meta中配置
class BlogPost(models.Model):
...
class Meta:
permissions = (
("can_publish", "Can Publish Posts"),
)
方法2 使用create()
函數
from myapp.models import BlogPost
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
content_type = ContentType.objects.get_for_model(BlogPost)
permission = Permission.objects.create(
codename='can_publish',
name='Can Publish Posts',
content_type=content_type,
)
在使用python manage.py migrate
命令後,就會創建這個新許可權,接著就可以在view中編寫程式碼判斷用戶是否有這個許可權來決定能否發表文章。
授權
可以在管理後台對用戶授權:

或者把用戶分組後,按組來進行授權:

從資料庫這6張表就能看出來,有用戶表、分組表、許可權表,以及它們的關聯關係表:

其程式碼實現是把permission賦值給User.user_permissions或者Group.permissions屬性。
代理模型許可權
代理模型是從某個模型繼承來的,不影響表結構,用於擴展行為實現程式碼解耦。
代理模型不會繼承父類的許可權,例如:
class Person(models.Model):
class Meta:
permissions = [('can_eat_pizzas', 'Can eat pizzas')]
# 代理模型
class Student(Person):
class Meta:
proxy = True
permissions = [('can_deliver_pizzas', 'Can deliver pizzas')]
>>> # 注意代理模型取ContentType需要加for_concrete_model=False
>>> content_type = ContentType.objects.get_for_model(Student, for_concrete_model=False)
>>> student_permissions = Permission.objects.filter(content_type=content_type)
>>> [p.codename for p in student_permissions]
['add_student', 'change_student', 'delete_student', 'view_student',
'can_deliver_pizzas'] # 沒有父類的can_eat_pizzas許可權
Session認證
Django認證系統是基於Session的。Django把Web請求封裝成了request(HttpRequest類),然後通過中間件設置了session相關的屬性:request.session、request.site、request.user。其中request.user就代表當前用戶,如果未登陸它的值是AnonymousUser(匿名用戶)的實例,如果已登陸它的值是User的實例。可以通過is_authenticated來判斷是否已認證:
if request.user.is_authenticated:
# Do something for authenticated users.
...
else:
# Do something for anonymous users.
...
用戶登錄
我們先簡單回顧一下基於session的登錄過程:

Django提供了login()
函數來登錄,把用戶憑證保存到session中。它的函數簽名如下:
login(request, user, backend=None)
示例:
import json
from django.contrib.auth import authenticate, login
from django.http import HttpResponse
# Create your views here.
def my_view(request):
request_body = json.loads(request.body)
username = request_body["username"]
password = request_body["password"]
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return HttpResponse("logged in")
else:
return HttpResponse("invalid login")
除了保存用戶憑證,Django還會把認證後端也保存到session中,便於相同的認證後端下次可以直接獲取到用戶資訊。至於保存哪個認證後端,Django按以下順序選取:
- 使用
login()
函數的backend參數值,如果賦值了的話。 - 使用user.backend的值,如果有的話。
- 使用settings中
AUTHENTICATION_BACKENDS
的值,默認['django.contrib.auth.backends.ModelBackend']
。 - 否則拋出異常。
用戶登出
Django提供了logout()
函數來登出。它的函數簽名如下:
logout(request)
示例:
from django.contrib.auth import logout
def logout_view(request):
logout(request)
# Redirect to a success page.
登出後session會被銷毀,所有數據都會被清除,以防止其他人使用相同的瀏覽器再次登錄後獲取到之前用戶的session數據。
login_required
對於未登陸的用戶,需要進行限制,必須先登陸才能進行訪問。
傳統方法
使用request.user.is_authenticated判斷,然後重定向到登錄頁面:
from django.conf import settings
from django.shortcuts import redirect
def my_view(request):
if not request.user.is_authenticated:
return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
# ...
或者錯誤頁面:
from django.shortcuts import render
def my_view(request):
if not request.user.is_authenticated:
return render(request, 'myapp/login_error.html')
# ...
login_required裝飾器
login_required(redirect_field_name='next', login_url=None)
示例:
from django.contrib.auth.decorators import login_required
@login_required
def my_view(request):
...
它的處理是這樣的:
-
如果用戶沒有登錄,就重定向到settings.LOGIN_URL(默認值
/accounts/login/
),同時把當前的絕對路徑添加到查詢字元串中,如:/accounts/login/?next=/polls/3/
。 -
如果用戶已經登錄了,正常執行view程式碼。
login_required
的redirect_field_name
參數是指登陸認證成功後重定向的頁面,默認保存在叫做next
的查詢字元串參數中(如/accounts/login/?next=/polls/3/)。可以修改為自定義:
from django.contrib.auth.decorators import login_required
@login_required(redirect_field_name='my_redirect_field')
def my_view(request):
...
不過修改後還需要同時修改login模板等。
login_required
的login_url
參數是指登錄頁面的url,可以自定義,默認是/accounts/login/
,需要在URLconf中關聯登陸視圖:
from django.contrib.auth import views as auth_views
path('accounts/login/', auth_views.LoginView.as_view()),
function views和class-based views
function views(函數視圖),視圖是個函數:
from django.http import HttpResponse
def my_view(request):
if request.method == 'GET':
# <view logic>
return HttpResponse('result')
class-based views(基於類的視圖),視圖是個類:
from django.views import View
class MyView(View):
def get(self, request):
# <view logic>
return HttpResponse('result')
為什麼需要cbv?因為類可以繼承,提高程式碼復用。由於Django的URLconf只能接受函數,所以cbv有個as_view()方法用來返回一個函數:
# urls.py
from django.urls import path
from myapp.views import MyView
urlpatterns = [
path('about/', MyView.as_view()),
]
LoginRequiredMixin
Mixin是為了程式碼復用,從多個父類繼承而來的類。如果使用的是class-based views,那麼可以使用LoginRequiredMixin,來實現login_required的效果,例如:
from django.contrib.auth.mixins import LoginRequiredMixin
class MyView(LoginRequiredMixin, View):
login_url = '/login/'
redirect_field_name = 'redirect_to'
permission_required
除了需要登錄,有些視圖還需要許可權。Django提供了permission_required
裝飾器,它的函數簽名如下:
permission_required(perm, login_url=None, raise_exception=False)
示例:
from django.contrib.auth.decorators import permission_required
@permission_required('polls.add_choice')
def my_view(request):
...
permission_required
的perm
參數,指的是許可權,可以是單個許可權,也可以是許可權列表。
permission_required
的login_url
參數和login_required
的login_url
作用一樣。
permission_required
的raise_exception
參數,可以用來拋出異常,賦值為True後會跳轉到403(HTTP Forbidden)頁面而非登錄頁面。
如果既想拋出異常 ,又想跳轉到登錄頁面,那麼可以同時添加這2個裝飾器:
from django.contrib.auth.decorators import login_required, permission_required
@login_required
@permission_required('polls.add_choice', raise_exception=True)
def my_view(request):
...
PermissionRequiredMixin
如果使用的是class-based views,那麼可以使用PermissionRequiredMixin,來實現permission_required的效果,例如:
from django.contrib.auth.mixins import PermissionRequiredMixin
class MyView(PermissionRequiredMixin, View):
permission_required = 'polls.add_choice'
# Or multiple of permissions:
permission_required = ('polls.view_choice', 'polls.change_choice')
修改密碼導致session失效
登錄成功後,Django會把加密後的密碼hash值存入session中,每次請求時,會校驗session中的密碼和資料庫中的密碼是否匹配。如果修改了密碼,資料庫中的密碼改變了,而session中的密碼沒有更新,那麼密碼就會匹配不上,導致session失效。django.contrib.auth的PasswordChangeView和user_change_password視圖會在修改密碼時更新session中的密碼hash,來避免session失效。如果對修改密碼的視圖進行了自定義,那麼可以使用update_session_auth_hash(request, user)
來更新session中的密碼,防止修改密碼導致session失效。
認證視圖
Django提供了登錄、登出、密碼管理等視圖。最簡單的使用方式是在URLconf中配置:
urlpatterns = [
path('accounts/', include('django.contrib.auth.urls')),
]
它會包含這些URL patterns:
accounts/login/ [name='login']
accounts/logout/ [name='logout']
accounts/password_change/ [name='password_change']
accounts/password_change/done/ [name='password_change_done']
accounts/password_reset/ [name='password_reset']
accounts/password_reset/done/ [name='password_reset_done']
accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/reset/done/ [name='password_reset_complete']
name是別名,可以使用reverse()函數來獲取,如
reverse('login')
。
但有時我們需要自定義url,在URLconf中添加自定義url後,再加上相應視圖即可,例如:
from django.contrib.auth import views as auth_views
urlpatterns = [
path('change-password/', auth_views.PasswordChangeView.as_view()),
]
所有的這些視圖都是class-based views,便於繼承後重寫進行自定義。
Django提供的相關視圖有LoginView、LogoutView、PasswordChangeView、PasswordChangeDoneView、PasswordResetView、PasswordResetDoneView、PasswordResetConfirmView、PasswordResetCompleteView。
快速上手體驗
如果想快速上手體驗,可以按如下步驟進行操作:
-
pip install django
,安裝Django。 -
django-admin startproject project_name
,創建Django項目。 -
python manage.py migrate
,數據遷移,使用自帶SQLite資料庫即可。 -
python manage.py createsuperuser
,創建超級管理員。 -
python manage.py runserver
,啟動項目。 -
訪問
//127.0.0.1:8000/admin/
,用超管登錄管理後台。
就可以使用Django自帶認證系統了。
小結
本文介紹了Django自帶的基於session的認證系統,闡述了用戶、組、認證與授權的相關概念,以及session認證的技術細節,最後講解了如何快速上手體驗的操作步驟。雖然如今基於session認證用的很少了,但它卻是理解Token、JWT認證的基礎,仍然值得我們學習。
參考資料: