Django認證系統並不雞肋反而很重要

dongfanger發表於2020-12-11

在使用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後,資料庫會新增認證系統的這些表:

Django認證系統並不雞肋反而很重要

認證與授權

認證的英文是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
  • email
  • 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 管理後臺建立

訪問http://127.0.0.1:8000/admin/,用超管登入後,在介面上建立:

Django認證系統並不雞肋反而很重要

修改密碼

方法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 管理後臺修改

Django認證系統並不雞肋反而很重要

Django認證系統並不雞肋反而很重要

使用者認證

框架底層使用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的認證:

Django認證系統並不雞肋反而很重要

許可權管理

許可權一般分為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中編寫程式碼判斷使用者是否有這個許可權來決定能否發表文章。

授權

可以在管理後臺對使用者授權:

Django認證系統並不雞肋反而很重要

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

Django認證系統並不雞肋反而很重要

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

Django認證系統並不雞肋反而很重要

其程式碼實現是把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認證系統並不雞肋反而很重要

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按以下順序選取:

  1. 使用login()函式的backend引數值,如果賦值了的話。
  2. 使用user.backend的值,如果有的話。
  3. 使用settings中AUTHENTICATION_BACKENDS的值,預設 ['django.contrib.auth.backends.ModelBackend']
  4. 否則丟擲異常。

使用者登出

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_requiredredirect_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_requiredlogin_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_requiredperm引數,指的是許可權,可以是單個許可權,也可以是許可權列表。

permission_requiredlogin_url引數和login_requiredlogin_url作用一樣。

permission_requiredraise_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。

快速上手體驗

如果想快速上手體驗,可以按如下步驟進行操作:

  1. pip install django,安裝Django。

  2. django-admin startproject project_name,建立Django專案。

  3. python manage.py migrate,資料遷移,使用自帶SQLite資料庫即可。

  4. python manage.py createsuperuser,建立超級管理員。

  5. python manage.py runserver,啟動專案。

  6. 訪問http://127.0.0.1:8000/admin/,用超管登入管理後臺。

就可以使用Django自帶認證系統了。

小結

本文介紹了Django自帶的基於session的認證系統,闡述了使用者、組、認證與授權的相關概念,以及session認證的技術細節,最後講解了如何快速上手體驗的操作步驟。雖然如今基於session認證用的很少了,但它卻是理解Token、JWT認證的基礎,仍然值得我們學習。

參考資料:

https://docs.djangoproject.com/en/3.1/topics/auth/

https://docs.djangoproject.com/en/3.1/topics/auth/default/

相關文章