Django筆記三十二之session登入驗證操作

2023-04-27 21:00:27

本文首發於公眾號:Hunter後端

原文連結:Django筆記三十二之session登入驗證操作

這一篇筆記將介紹 session 相關的內容,包括如何在系統中使用 session,以及利用 session 實現登入認證的功能。

這篇筆記將分為以下幾個內容:

  1. session 的使用流程
  2. session 的設定和相關方法
  3. users 模組的準備
  4. session 驗證的的實現
  5. Session 表介紹
  6. 登入驗證的幾種實現形式

1、session 的使用流程

cookie 和 session 的基本概念這裡不做贅述,這裡簡單講一下在 Django 中如何使用自定義的模組來實現登入、登出以及僅允許登入使用者存取某些介面的操作。

Django 有一套自帶的 auth 驗證模組,包括使用者以及使用者及相應的許可權的表和操作,我們這裡沒有用,而是單獨自定義一個 user 模組以及相應的功能函數用來實現使用者的註冊、登入和登出功能。

session 在這裡的使用流程大致如下:

1、通過 login 介面,驗證成功後,將某些資訊寫入 session,可以是 user_id,或者是某個你自定義的特定的欄位,反正是後續需要進行驗證是否登入成功的資料

2、在存取特定的、需要登入才可檢視的介面前,先檢查前端返回的資料中是否包含我們在上一步中寫入的資料來確保使用者是處於登入狀態,如果是,則允許繼續存取,否則返回未登入的資訊,提示使用者需要先進行登入操作

3、通過 logout 介面,將使用者在 login 介面裡寫入的登入資訊抹除,返回登出成功資訊

在 Django 中,系統自動為我們準備好了 session 的所有相關的操作,我們只需要在後續的登入操作中往裡面寫入我們需要驗證的資料即可。

Django 這部分為我們準備好的 session 操作也是通過中介軟體的形式存在的,是 settings.py 的 MIDDLEWARE 的 'django.contrib.sessions.middleware.SessionMiddleware'

如果不指定其他儲存方式,session 的資料預設存在於我們的後端表中,這個我們在第一次執行 migrate 的時候已經自動為我們建立了該表,名為 django_session

表資料的操作和檢視我們在後面再詳細介紹。

2、session 的設定和相關方法

前面已經介紹了 session 的操作流程,這裡我們介紹一下 session 的相關設定和方法。

session 設定

以下設定都在 settings.py 中設定,事實上,這些 session 的預設設定就差不多可以使用,後續有特殊需求我們可以再來檢視,這裡只介紹幾個我覺得方便我們使用的。

這個地方的官方檔案地址在:https://docs.djangoproject.com/zh-hans/3.2/ref/settings/

SESSION_COOKIE_AGE

session 過期時間,預設為 1209600,即 14 * 24 * 60 * 60,為 14天。

我們可以在 settings.py 中設定 session 的過期時長,也可以在程式中使用方法手動設定過期時長,方法的使用我們後面再介紹。

SESSION_COOKIE_NAME

預設值為 sessionid,在使用者登入之後,請求我們系統,請求的 cookie 裡會帶上 session key-value 的引數,這個 key 就是我們這裡的 SESSION_COOKIE_NAME,預設為 sessionid。

如果想改成其他的名稱直接定義即可。

SESSION_ENGING

Django 儲存 session 具體資料的地方,預設值為 django.contrib.sessions.backends.db,表示存在於資料庫,也就是我們前面說的在 django_session 這張表。

也可以儲存在檔案或者快取裡。

session 方法

這裡接著介紹一下 session 相關的方法,這些方法的呼叫一般是在介面裡通過 request.session 來操作。

這裡我們只是做一下方法的作用和效果的介紹,具體用途我們在之後的範例中再詳細說明。

dict 操作

我們可以將 request.session 視作一個 dict,往裡面新增 user_id,is_login 等用於標識使用者是否登入的資訊的時候可以直接操作,比如:

request.session["user_id"] = 1
request.session["is_login"] = True

keys()

輸出 request.session.keys() 返回的就是我們在前面往 session 裡新增的資料。

同理,request.session.items() 輸出的也是我們往裡新增的資料的 key-value 的值。

del 操作

當我們使用登出操作時,可以直接使用:

del request.session["user_id"]

這種方式會刪除 session 中我們儲存的 user_id 資訊,這樣使用者在存取我們的介面的時候,如果我們做登入驗證的操作,就會找不到已經登入的資訊。

之前我們說過,我們的 session 資料會儲存在資料庫裡,這種方式僅僅是刪除 session 中某個特定的 key-value,並不會刪除 django_session 表中這條資料

而如果想要直接刪除這一條 session 資料,則可以使用 flush() 方法

flush()

下面的操作則會直接運算元據庫刪除這條 session 資料:

request.session.flush()

flush() 和 前面的 del 方法都可以用作我們 logout 過程中的操作。

get_expiry_age()

獲取 session 過期秒數,這個值就是前面我們在 settings.py 中設定的 SESSION_COOKIE_AGE 的值。

clear_expired()

從 django_session 中移除過期的對談,下面會介紹 Session 這個 model 的相關操作,這裡提前說一下這個函數。

django_session 會有一個 expire_date 欄位,clear_expired() 這個操作就會刪除表裡 expire_date 小於當前時間的資料。

3、users 模組的準備

前面介紹了 session 的相關設定和方法以及 session 的基本使用流程。接下來我們將介紹如何在系統中使用上 session。

在介紹 session 使用前,我們自定義一個 users application 來做一下相關準備。

新建一個 application 和 相關的設定,在前面的筆記中都有介紹,這裡不再做贅述,比如 app 的建立、在 settings.py 裡 INSTALLED_APPS 裡的定義,和 hunter/urls.py 的 patterns 裡新建一條資料,指向 users/urls.py 等操作。

其中,在 hunter/urls.py 中對 users app 的 url 字首我們定義為 users,如下:

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
    path('users/', include('users.urls')),
]

我們這裡在 users/models.py 下新建一個 User model,然後對其進行相關的 migration 操作,使其表新增到資料庫中。

# users/models.py

from django.db import models


class User(models.Model):
    username = models.CharField(max_length=20, verbose_name="登入使用者名稱", unique=True)
    password = models.CharField(max_length=256, verbose_name="加密密碼")

4、session 驗證的的實現

接下來,我們將新建幾個介面:

  • 使用者註冊介面
  • 使用者登入介面
  • 使用者登出介面
  • 使用者資訊介面

可以先看下這幾個介面的程式碼總攬,接著我們詳細介紹一下介面的操作。

users/urls.py

from django.urls import path
from users.views import LoginView, RegisterView, LogoutView, UserInfoView

urlpatterns = [
    path("register", RegisterView.as_view()),
    path("login", LoginView.as_view()),
    path("logout", LogoutView.as_view()),
    path("user/info", UserInfoView.as_view()),
]
users/views.py

from django.contrib.auth.hashers import make_password, check_password
from django.http import JsonResponse
from django.views import View
from users.models import User
import json


# 使用者註冊
class RegisterView(View):
    def post(self, request):
        request_json = json.loads(request.body)
        username = request_json.get("username")
        password = request_json.get("password")

        if not username or not password:
            result = {"code": -1, "msg": "username or password not valid"}
        else:
            if User.objects.filter(username=username).exists():
                result = {"code": -1, "msg": "username exists"}
            else:
                User.objects.create(username=username, password=make_password(password))
                result = {"code": 0, "msg": "success"}
        return JsonResponse(result, safe=False)


# 使用者登入
class LoginView(View):
    def post(self, request):
        request_json = json.loads(request.body)
        username = request_json.get("username")
        password = request_json.get("password")

        if not username or not password:
            result = {"code": -1, "msg": "login info error"}
        else:
            user = User.objects.filter(username=username).first()
            if not user:
                result = {"code": -1, "msg": "username not found"}
            else:
                if check_password(password, user.password):
                    result = {"code": 0, "msg": "success"}
                    request.session["username"] = username
                else:
                    result = {"code": -1, "msg": "password error"}

        return JsonResponse(result, safe=False)


# 使用者登出
class LogoutView(View):
    def post(self, request):
        if request.session.get("username"):
            del request.session["username"]
            # request.session.flush()
        return JsonResponse({"code": 0, "msg": "登出成功"})


# 使用者資訊
class UserInfoView(View):
    def post(self, request):
        username = request.session.get("username")
        if username:
            result = {"code": 0, "msg": f"登入使用者為{username}"}
            status = 200
        else:
            result = {"code": -1, "msg": "使用者未登入"}
            status = 401
        return JsonResponse(result, status=status)

首先介紹一下,所有請求的引數都是放在 body 裡以 json 格式傳遞,我這裡都是通過 postman 來請求測試的。

其次,在請求裡,session 的處理可以直接通過 request.session 的方式進行,以下見範例。

使用者註冊介面

在註冊介面裡,這裡做了引數校驗的簡化,直接 json.loads() 處理 body 的內容,然後通過 Django 自帶的加密函數 make_password 將密碼以加密的形式儲存。

使用者登入介面

登入介面裡,首先是校驗賬號密碼是否正確,判斷正確後我們將登入使用者的 username 欄位寫入 session,然後在使用者下一次請求的時候就會自動獲取該 session。

或者更正確的來說,使用者登入在操作 request.session 之後,在返回 response 的時候,系統會在 django_session 裡新增或者更新該使用者的記錄,這條資料有包含 session_key,session_data 和 expire_date 這幾個欄位。

session_key,在 cookie 的名稱是 sessionid,postman 中第一次登入之後,在之後的每一次介面請求都會將sessionid=xx 傳給後端,後端就會根據這個 session_key 的值去 django_session 表裡查詢相應的記錄

如果這個 session_key 在表裡不存在記錄,或者 expire_date 過期了,那麼後端系統會自動給其值賦為 None,即認定此次介面請求是未登入狀態。

expire_date 欄位則是一個時間欄位,主要用於判斷資料是否過期。

session_data 則是會包含我們寫入的資料,比如我們在使用者登入的時候,通過 request.session["username"] = username 的方式寫入了一些特殊的標識,然後將其編碼成 session_data 的值存入資料庫,那麼使用者在下次請求介面的時候我們就可以通過解碼 session_data,將值取出來用於判斷使用者是否登入。

將 session_data 解碼的方式可以單獨通過獲取 django_session 的記錄然後獲取,但是在請求中,Django 為我麼做了這些解碼工作,我們可以直接通過前面介紹的 request.session.items() 的方式來檢視在當前登入的 session_data 裡寫入的 key-value 資料。

注意: 前後端並不直接將 session_data 作為值傳遞,而是會傳遞 session_key 這個引數,一些校驗的資料也都是放在 session_key 對應記錄的 session_data 中存在後臺的資料庫中。

使用者資訊介面

我們假定獲取使用者資訊介面要求使用者必須處於登入狀態,實際上也是,因為使用者不登入無法定位到使用者,然後獲取使用者的資訊。

那麼我們在進行下一步的實際操作前,我們肯定需要嘗試從 session 中獲取使用者相應的資訊,如果獲取到了,則判斷是處於登入狀態,否則是處於未登入狀態,無法獲取使用者資訊。

所以我們這裡的判斷是從 session 中獲取 username 欄位,通過判斷 username 是否有值來判斷使用者是否處於登入狀態。

使用者登出介面

使用者登出,也就是登出介面,我們這裡用的是 del 的方式,這個主要是看我們驗證使用者登入的方式,比如我們是通過向 session 中取值 username 來判斷使用者是否登入,那麼 del request.session["username"] 的操作即可實現登出的功能。

注意: 這裡執行的 del 操作僅僅是刪除 session_data 中的 {"username": "xxx"} 的資料,這條 session_key 對應的資料還存在。

可以看到,在這條程式碼的下一行還有一條是執行的 flush() 操作,這個操作是直接在資料庫裡刪除這條 session 記錄,這是一種更為徹底的登出操作。

這裡還需要注意的一點是,del 操作的前提是 session 資料裡必須要有 username 這個 key,否則會引起報錯,所以我們這裡用了一個 if 判斷邏輯,我們還可以使用 try-except 操作,或者更為徹底的操作是直接使用 flush() 操作。

至此,使用者登入登出以及 session 資料的基本使用操作就介紹完畢了,下面我們額外介紹一些操作。

5、Session 表介紹

django_session 表的單獨獲取檢視操作一般在程式裡不會出現,因為前後端都是通過 cookie 中 sessionid 直接獲取到對應的資料,但為了以防萬一,或者你對這張表有一些興趣,這裡額外介紹一下如何單獨操作這張表裡的資料。

django_session 表的引入方式如下:

from django.contrib.sessions.models import Session

然後通過 session_key 來獲取這條資料,比如 session_key 為 nqu3s71e38279bl5cbgju6sut64tnqmx,就可以:

session_key = "nqu3s71e38279bl5cbgju6sut64tnqmx"

session = Session.objects.get(pk=session_key)
# session = Session.objects.get(session_key=session_key)

其中,我們向 session 裡寫入的資料都包含在 session.session_data 裡,我麼可以直接通過 get_decoded() 方法來獲取:

session.get_decoded()

# {'username': 'root'}

6、登入驗證的幾種實現形式

獲取使用者資訊這個介面需要使用者登入才可以接著獲取使用者資訊,我們這裡的操作是直接判斷 session 裡是否含有 username 欄位。

但是如果我們系統裡大部分介面都是需要使用者先登入才可存取,這樣在每個 views 裡都要先加這個判斷的操作,這樣的顯然是不實際的。

那麼我們可以怎麼操作來實現這個重複性的操作呢?

這裡提供兩個方式,一個是裝飾器,一個是寫在中介軟體裡。

裝飾器實現登入驗證

其實如果直接使用 Django 自帶的登入驗證的功能,是可以直接使用系統自帶的裝飾器的,但是我們這裡的表都是手動操作的,所以這個功能的裝飾器我這裡就自己實現了一個,相關程式碼如下:

def login_required_manual(func):
    def wrapper(*args, **kwargs):
        request = args[1]
        if not request.session.get("username"):
            return JsonResponse({"code": -1, "msg": "not login"}, status=401)
        return func(*args, **kwargs)
    return wrapper


class UserInfoView(View):
    @login_required_manual
    def post(self, request):
        username = request.session.get("username")
        return JsonResponse({"code": 0, "msg": f"登入使用者{username}"})

可以看到,使用了登入驗證的裝飾器之後,我們的程式碼都簡潔了很多。

我們可以嘗試在呼叫登出介面後,再呼叫使用者資訊介面,可以看到系統就自動返回了未登入的資訊了。

中介軟體實現登入驗證

這裡我們假定目前僅僅是註冊和登入不需要登入即可存取,然後我們建立一箇中介軟體如下:

# hunter/middlewares/auth_middleware.py

from django.http import JsonResponse

class AuthMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        path = request.path

        # url 路徑為 /users/register 和 /users/login 的介面不需要進行判斷驗證
        if path not in [
            "/users/register",
            "/users/login",
        ]:
            session = request.session
            if not session.get("username"):
                return JsonResponse({"code": -1, "msg": "not login"}, status=401)

        response = self.get_response(request)
        return response

然後在 hunter/settings.py 里加上這個中介軟體:

# hunter/settings.py

INSTALLED_APPS = [
    ...
    'hunter.middlewares.auth_middleware.AuthMiddleware',
    ...
]

這樣,在每個介面請求到達 views 檢視前,都會經歷這個驗證的中介軟體,這裡將介面路徑的判斷簡化成註冊介面和登入介面,這兩個介面不需要登入即可存取,其他介面都設定成需要登入才可存取。

相比於裝飾器的做法,這裡更推薦中介軟體的操作方式,這樣首先就不用在每個 views 前加上裝飾器,另外,需要登入才可存取的介面都可以在中介軟體部分統一列舉出來,方便檢視。

以上就是本篇筆記關於 session 的全部內容。

如果想獲取更多後端相關文章,可掃碼關注閱讀: