DRF之JWT簽發Token原始碼分析

ssrheart發表於2024-04-23

DRF之JWT簽發Token原始碼分析

【一】JWT介紹

  • JWT(JSON Web Token)是一種用於身份認證和授權的開放標準(RFC 7519)。
  • 它基於JSON格式定義了一種安全的令牌,用於在客戶端和伺服器之間傳輸資訊。

【二】JWT三段式

  • JWT(JSON Web Token)是一種用於身份認證和授權的開放標準(RFC 7519)。
  • 它基於JSON格式定義了一種安全的令牌,用於在客戶端和伺服器之間傳輸資訊。

JWT由三個部分組成:頭部(Header)、載荷(Payload)和簽名(Signature):

【1】頭部(Header):

  • 頭部通常由兩部分組成:
    • 令牌型別和演算法。
    • 令牌型別通常為"JWT"。
    • 演算法定義了用於生成簽名的演算法,例如HMAC、RSA或者ECDSA等。
  • 示例:
{
  "alg": "HS256",
  "typ": "JWT"
}

【2】載荷(Payload):

  • 載荷包含了關於使用者或實體的宣告和其他附加資訊。
  • JWT規範定義了一些標準的宣告(例如:iss-簽發者、exp-過期時間、sub-主題、aud-受眾),並且允許自定義宣告。
  • 示例:
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

【3】簽名(Signature):

  • 簽名是將頭部和載荷進行簽名,以確保令牌的完整性和真實性。
    • 簽名通常使用金鑰進行加密,以防止其被篡改。
    • 伺服器在接收到請求時使用同樣的金鑰對簽名進行驗證。
  • 示例:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secretKey
)

【4】最終的JWT

  • 由上述三個部分用.拼成一個完整的字串,構成最終的JWT

【三】JWT開發流程

【1】JWT工作流程如下:

  1. 使用者向伺服器傳送登入請求,提供使用者名稱和密碼。
  2. 伺服器驗證使用者憑證,如果透過驗證,生成JWT並將其返回給客戶端。
  3. 客戶端收到JWT後,儲存在本地(通常在本地儲存或者Cookie中)。
  4. 客戶端每次訪問受限資源時,在請求頭中附帶JWT。
  5. 伺服器接收到請求後,使用相同的金鑰解析JWT,驗證其有效性和完整性,然後根據需要執行相應的操作。

【2】JWT開發流程

第一部分:簽發token的過程(登入)

  • 使用者攜帶使用者名稱和密碼,訪問我,我們校驗透過,生成token串,返回給前端
  • 使用者攜帶使用者名稱和密碼訪問應用後端。
  • 應用後端校驗使用者提供的使用者名稱和密碼是否正確。
  • 如果校驗透過,應用後端生成一個JWT Token,並將其返回給前端。
    • JWT Token包含三個部分:Header、Payload和Signature。
    • Header包含了關於Token型別和所使用的演算法的資訊。
    • Payload包含了要傳遞的資料,例如使用者ID、角色等。
    • Signature是使用金鑰對Header和Payload進行簽名的結果,用於驗證Token的真實性。
    • 這些部分通常會經過Base64編碼組合成一個字串。
  • 前端收到JWT Token後,可以將其儲存在本地
    • 例如LocalStorage或者Cookie中,在後續需要使用Token的請求中攜帶它。

第二部分:token認證過程

  • token認證過程,登入認證時使用,其實就是咱們之前講的認證類,在認證類中完成對token的認證操作
  • 使用者訪問我們需要登陸後才能訪問的介面,必須攜帶我們簽發的token串(請求頭)
  • 我們取出token,驗證該token是否過期,是否被篡改,是否是偽造的
  • 如果正常,說明荷載中的資料,就是安全的,可以根據荷載中的使用者id,查詢出當前登入使用者,放到request中即可
  • 使用者訪問需要登入才能訪問的介面,請求頭中攜帶JWT Token。
  • 後端從請求頭中獲取JWT Token。
  • 後端對Token進行解析和驗證,確保Token的完整性和真實性。
    • 驗證Token的完整性可以透過驗籤來實現,即使用金鑰對Token的簽名部分進行驗證。
    • 驗證Token的真實性可以透過檢查Token的有效期以及其他業務邏輯來實現。
  • 如果Token驗證透過,後端能夠從Token的Payload部分獲取到使用者的相關資訊,例如使用者ID。
  • 後端可以根據使用者ID查詢資料庫或其他儲存系統,獲取該使用者的詳細資訊。
  • 後端將獲取到的使用者資訊新增到請求的上下文中,以便後續的處理邏輯可以使用該資訊進行許可權控制、資料處理等操作。

【四】JWT的優點

  • 簡潔:由於使用JSON格式,JWT具有易讀性和可理解性。
  • 自包含:JWT中包含了所有必要的資訊,減少伺服器端的儲存開銷。
  • 可擴充套件:JWT允許自定義宣告來滿足特定需求。
  • 跨域支援:JWT可以在不同域之間進行傳遞,並實現跨域認證。

然而,使用JWT需要注意以下幾點:

  • JWT無法撤銷:一旦JWT被簽發,就無法主動撤銷,只能等待過期時間到達或者透過其他方式進行處理。
  • 令牌大小:由於JWT包含載荷資訊,其大小較大,可能會影響網路傳輸和儲存開銷。

總結來說,JWT是一種靈活且安全的身份認證和授權機制,可以用於構建分散式系統和跨域認證場景。但在使用時需注意安全性和令牌的大小。

【五】djangorestframework-jwt自動簽發token

【1】安裝

pip3 install djangorestframework-jwt

【2】簽發過程

  • 登入過程(快速簽發)
    • 登入介面
      • 在URL路由中新增登入介面路徑,使用obtain_jwt_token函式進行使用者身份驗證並簽發JWT Token。
    • 基於 auth 的user表簽發
from django.contrib import admin
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', obtain_jwt_token),  # 登入介面有了,並且可以簽發token
]
  • {{host}}login/

    {
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjkwNzk5OTE0LCJlbWFpbCI6IiJ9.2KlbMxOsa1V7LDGzY2wQlWfJWvqCjEV4SSLtllnec_U"
    }
    

【3】總結

【1】簽發:只需要在路由中配置

from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
    path('login/', obtain_jwt_token), 
]
  • 在路由中配置登入介面路徑,使用obtain_jwt_token函式進行使用者身份驗證並簽發JWT Token。

【2】認證:檢視類上加

from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated


class BookView(APIView):
    authentication_classes = [JSONWebTokenAuthentication] # 認證類,drf-jwt提供的
    permission_classes = [IsAuthenticated] # 許可權類,drf提供的
  • 使用JWT認證方法需要在檢視類中新增相應的認證類和許可權類。
    • 認證類:JSONWebTokenAuthentication,該類提供了JWT的認證功能。
    • 許可權類:IsAuthenticated,該類用於驗證請求是否來自已認證的使用者。
  • 訪問的時候,要在請求頭中攜帶,必須叫
    • Authorization:jwt token串

【4】Django + JWT 自定製返回格式

  • 登入簽發token的介面,要返回code,msg,username,token等資訊

  • 寫個函式,函式返回字典格式,返回的格式,會被序列化,前端看到

    def common_response(token, user=None, request=None):
        return {
            'code': '100',
            'msg': '登入成功',
            'username': user.username,
            'token': token,
        }
    
  • 寫的函式配置一下

    JWT_AUTH = {
        'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.jwt_response.common_response',
        'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),# 設定token過期時間,預設5分鐘過期
    }
    

【六】Django + JWT 自定義使用者表簽發認證(手動簽發)

【1】簽發部分

【1】建立使用者表

from django.db import models


# Create your models here.
class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=255)
    age = models.IntegerField()

【2】登入介面

  • 路由
path('login/', views.UserView.as_view({"post": "login"})), 
  • 檢視
# 自定義使用者表,寫登入介面做 token 簽發
from rest_framework.viewsets import ViewSet
from app01 import models
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


class UserView(ViewSet):
    back_dict = {"code": 100, "msg": ""}

    def login(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        user_obj = models.User.objects.filter(username=username, password=password).first()
        if user_obj:
            # 登陸成功,簽發token
            # (1)透過user獲取荷載(payload)
            payload = jwt_payload_handler(user_obj)
            print(payload) # {'user_id': 1, 'username': 'dream', 'exp': datetime.datetime(2023, 7, 31, 11, 45, 10, 630745)}
            # (2) 透過荷載獲得 token
            token = jwt_encode_handler(payload)
            self.back_dict['code'] = 100
            self.back_dict['msg'] = "使用者登入成功"
            self.back_dict['username'] = user_obj.username
            self.back_dict['token'] = token
            return Response(self.back_dict)
        else:
            # 登陸失敗
            self.back_dict['code'] = 102
            self.back_dict['msg'] = "使用者名稱或密碼錯誤"
            return Response(self.back_dict)
  • 攜帶錯誤資訊

    • {{host}}login/
    {
        "username":"admin",
        "password":521
    }
    
    {
        "code": 102,
        "msg": "使用者名稱或密碼錯誤"
    }
    
  • 攜帶正確資訊

    • {{host}}login/
    {
        "username":"dream",
        "password":521
    }
    
    
    {
        "code": 100,
        "msg": "使用者登入成功",
        "username": "dream",
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImRyZWFtIiwiZXhwIjoxNjkwODA0MDMzfQ.VGcEd0HkMH4aAG_2EoorOx90Rw8G5bPGe4eGWaaDgI4"
    }
    

【2】認證部分

  • drf的認證類定義方式
  • 在認證類中,自己寫邏輯
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.settings import api_settings
import jwt
from app01 import models

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER


class JWTAuthentication(BaseAuthentication):
    #
    def authenticate(self, request):
        # 獲取到請求頭中攜帶的 token : 可自定義名字
        token = request.META.get('HTTP_TOKEN')
        # 校驗token是否過期

        # 校驗token是否合法
        try:
            payload = jwt_decode_handler(token)
            # 如果認證透過,payload就可以認為是安全的,我們就可以使用
            user_id = payload.get('user_id')

            # 每個需要登入後才能訪問的介面,都會走這個認證類
            # 一旦走一次就要去資料庫中查詢一次,這樣會對資料庫造成壓力
            # user = models.User.objects.filter(pk=user_id)

            # 最佳化 --- 除了指定欄位,其他欄位不能校驗
            user = models.User(username=payload.get('username'), user_id=user_id)
        except jwt.ExpiredSignature:
            raise AuthenticationFailed("token超時")
        except jwt.DecodeError:
            raise AuthenticationFailed("解碼失敗")
        except jwt.InvalidTokenError:
            raise AuthenticationFailed("token異常")
        except Exception:
            raise AuthenticationFailed("token認證異常")

        return user, token
  • 檢視
from app01.JWT_ap.jwt_authentication import JWTAuthentication


class UserInfoView(ViewSet):
    back_dict = {"code": 100, "msg": ""}
    authentication_classes = [JWTAuthentication]

    def login(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        user_obj = models.User.objects.filter(username=username, password=password).first()
        if user_obj:
            # 登陸成功,簽發token
            # (1)透過user獲取荷載(payload)
            payload = jwt_payload_handler(user_obj)
            print(
                payload)  # {'user_id': 1, 'username': 'dream', 'exp': datetime.datetime(2023, 7, 31, 11, 45, 10, 630745)}
            # (2) 透過荷載獲得 token
            token = jwt_encode_handler(payload)
            self.back_dict['code'] = 100
            self.back_dict['msg'] = "使用者登入成功"
            self.back_dict['username'] = user_obj.username
            self.back_dict['token'] = token
            return Response(self.back_dict)
        else:
            # 登陸失敗
            self.back_dict['code'] = 102
            self.back_dict['msg'] = "使用者名稱或密碼錯誤"
            return Response(self.back_dict)
  • 路由
path('books/', views.UserInfoView.as_view({"post": "login"})),  # 登入介面有了,並且可以簽發token
  • 不攜帶token/錯誤toke

    {
        "detail": "解碼失敗"
    }
    

【七】rest_framework_jwt原始碼分析

【1】流程分析

# from rest_framework_jwt.views import obtain_jwt_token
# obtain_jwt_token就是ObtainJSONWebToken.as_view()---》檢視類.as_view()
  • 引入了rest_framework_jwt模組,並且使用了obtain_jwt_token函式,該函式實際上是ObtainJSONWebToken.as_view()方法的別名。

  • JSONWebTokenAPIView
    

    是一個API檢視類,繼承自

    rest_framework
    

    框架的

    APIView
    

    類。

    • 它用於處理接收到的包含使用者使用者名稱和密碼的POST請求,並返回一個JSON Web Token,該令牌可以用於後續的身份驗證請求。
  • obtain_jwt_token---->ObtainJSONWebToken.as_view()
class ObtainJSONWebToken(JSONWebTokenAPIView):
    """
    API View that receives a POST with a user's username and password.

    Returns a JSON Web Token that can be used for authenticated requests.
    """
    serializer_class = JSONWebTokenSerializer
  • JSONWebTokenAPIView
    

    serializer_class
    

    屬性指定了用於序列化和驗證使用者輸入的資料的序列化器類

    • 這裡使用的是JSONWebTokenSerializer
  • JSONWebTokenAPIView
class JSONWebTokenAPIView(APIView):
    """
    Base API View that various JWT interactions inherit from.
    """
    # 區域性禁用許可權和認證
    permission_classes = ()
    authentication_classes = ()

    def get_serializer_context(self):
        """
        Extra context provided to the serializer class.
        """
        return {
            'request': self.request,
            'view': self,
        }

    def get_serializer_class(self):
        """
        Return the class to use for the serializer.
        Defaults to using `self.serializer_class`.
        You may want to override this if you need to provide different
        serializations depending on the incoming request.
        (Eg. admins get full serialization, others get basic serialization)
        """
        assert self.serializer_class is not None, (
            "'%s' should either include a `serializer_class` attribute, "
            "or override the `get_serializer_class()` method."
            % self.__class__.__name__)
        return self.serializer_class

    def get_serializer(self, *args, **kwargs):
        """
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        kwargs['context'] = self.get_serializer_context()
        return serializer_class(*args, **kwargs)
	
    # post 請求:簽發簽名
    def post(self, request, *args, **kwargs):
        # serializer = JSONWebTokenSerializer(data=request.data)
        serializer = self.get_serializer(data=request.data)
		# 呼叫序列化類的 is_valid()
        # 欄位自己的校驗規則,區域性鉤子/全域性鉤子
        if serializer.is_valid(): # 全域性鉤子校驗引數,生成token
            # 從序列化類中取出 user
            user = serializer.object.get('user') or request.user
            # 從序列化類中取出 token
            token = serializer.object.get('token')
            # 定製返回格式時,重寫了 jwt_response_payload_handler 方法
            response_data = jwt_response_payload_handler(token, user, request)
            # 返回字典
            response = Response(response_data)
            if api_settings.JWT_AUTH_COOKIE:
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  • permission_classes
    

    authentication_classes
    

    欄位:

    • 這兩個欄位用於定義API檢視的許可權和認證類,預設情況下為空元組,即沒有任何限制和認證要求。
  • get_serializer_context
    

    方法:

    • 此方法返回給序列化器類的額外上下文資訊。
    • 預設情況下,提供了requestview資訊作為上下文。
  • get_serializer_class
    

    方法:

    • 該方法返回用於序列化的類。
    • 預設情況下使用self.serializer_class欄位的值。
    • 如果需要根據請求的不同提供不同的序列化方式,可以重寫此方法。
  • get_serializer
    

    方法:

    • 該方法返回一個序列化器的例項,用於驗證、反序列化輸入和序列化輸出。
    • 首先透過get_serializer_class方法獲取序列化器類。
    • 然後將額外的上下文資訊傳遞給序列化器例項的context引數。
    • 最後返回序列化器的例項。
  • post
    

    方法:

    • 這是處理POST請求的方法,負責簽發JWT令牌。
    • 首先透過self.get_serializer(data=request.data)獲取序列化器的例項。
    • 利用序列化器的is_valid()方法對請求資料進行校驗,包括全域性鉤子和欄位自定義的校驗規則。
    • 如果校驗透過,從序列化器中獲取使用者物件和令牌物件。
    • 接下來呼叫jwt_response_payload_handler方法自定義返回格式,並將令牌、使用者和請求作為引數傳遞給它,獲取返回資料字典。
    • 建立一個響應物件,將返回資料作為內容進行響應。
    • 如果配置檔案中使用了JWT的cookie認證(api_settings.JWT_AUTH_COOKIE),則設定JWT的cookie,設定過期時間為當前時間加上JWT的過期時間差(api_settings.JWT_EXPIRATION_DELTA)。
    • 最後返回響應物件。
    • 如果校驗失敗,返回錯誤資訊和HTTP 400錯誤狀態碼的響應。
  • JSONWebTokenSerializer的全域性鉤子/區域性鉤子
    • 全域性鉤子
def validate(self, attrs):
    # attrs : 前端傳入校驗過的資料{username:dream,password:521}
    credentials = {
        # 獲取 username
        self.username_field: attrs.get(self.username_field),
        # 獲取password
        'password': attrs.get('password')
    }
	
    # 全部為真 : 檢驗 credentials中字典的 value 全部有值
    if all(credentials.values()):
        # user = authenticate(將前端傳入的資料全部打散)
        # user=authenticate(username=前端傳入的,password=前端傳入的)
        # authenticate : auth模組的使用者名稱和密碼認證函式,可以傳入使用者名稱和密碼,去auth的user表中校驗使用者是否存在
        # 等同於:User.object.filter(username=username,password=加密後的密碼).first()
        user = authenticate(**credentials)

        if user:
            if not user.is_active:
                msg = _('User account is disabled.')
                raise serializers.ValidationError(msg)

            payload = jwt_payload_handler(user)

            return {
                'token': jwt_encode_handler(payload),
                'user': user
            }
        else:
            msg = _('Unable to log in with provided credentials.')
            raise serializers.ValidationError(msg)
    else:
        msg = _('Must include "{username_field}" and "password".')
        msg = msg.format(username_field=self.username_field)
        raise serializers.ValidationError(msg)
  • 上述程式碼是一個JSONWebTokenSerializer的全域性鉤子(validate方法)。
    • 在該程式碼段中,validate方法接收前端傳入的經過驗證的資料(attrs),然後提取使用者名稱和密碼,並進行使用者認證。
  • 首先,定義了一個credentials字典,其中包含使用者名稱(self.username_field)和密碼('password')。
    • 透過attrs.get()方法從前端傳入的資料中獲取對應的值,並將其賦給credentials字典中的相應鍵。
  • 接下來,使用all()函式判斷credentials字典中的值是否全部存在(即使用者名稱和密碼都不為空)。
    • 如果是,則呼叫authenticate()函式進行使用者認證。
  • authenticate()函式是在Django的auth模組中進行使用者名稱和密碼認證的函式。
    • 它接收使用者名稱和密碼作為引數,在auth的user表中校驗使用者是否存在。
    • 相當於執行了類似於User.objects.filter(username=username, password=加密後的密碼).first()的查詢,返回使用者物件。
  • 如果使用者認證成功,繼續進行進一步的操作。
    • 首先判斷使用者是否啟用,如果使用者未啟用,則丟擲ValidationError異常,提示使用者賬戶已被禁用。
  • 接著,生成payload(有效載荷)物件,該物件包含了使用者的資訊。然後,透過jwt_encode_handler()函式對payload進行加密處理,得到一個token。
    • 最後,將token和user物件作為字典返回。
  • 如果使用者認證失敗,則丟擲ValidationError異常,提示無法使用提供的憑據登入。
  • 如果credentials字典中的值存在空值(即使用者名稱或密碼為空),則丟擲ValidationError異常,提醒必須包含使用者名稱和密碼。
  • 總結:該全域性鉤子的作用是對前端傳入的資料進行校驗和使用者認證,並返回包含token和使用者資訊的字典。如果驗證或認證失敗,則丟擲相應的異常。
  • jwt_response_payload_handler(token, user, request)
    
    • 定製返回資料格式
jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER

【2】原始碼分析

  • BaseJSONWebTokenAuthentication
from rest_framework_jwt.settings import api_settings

# 從DRF JWT設定中匯入了JWT解碼處理器和獲取使用者名稱的處理器
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER


class BaseJSONWebTokenAuthentication(BaseAuthentication):
    """
    Token based authentication using the JSON Web Token standard.
    """
	
    # 這個方法用於驗證JWT令牌並返回使用者和令牌的元組,如果令牌有效。如果令牌無效,則返回 None。
    def authenticate(self, request):
        """
        Returns a two-tuple of `User` and token if a valid signature has been
        supplied using JWT-based authentication.  Otherwise returns `None`.
        """
        # 從請求中獲取JWT令牌值
        jwt_value = self.get_jwt_value(request)
        if jwt_value is None:
            return None

        try:
            # 使用JWT解碼處理器解碼JWT令牌,並獲取其中的載荷(payload)
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            # 如果令牌過期,則丟擲 AuthenticationFailed 異常
            msg = _('Signature has expired.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            # 如果解碼時出現錯誤,丟擲 AuthenticationFailed 異常
            msg = _('Error decoding signature.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            # 如果令牌無效,丟擲 AuthenticationFailed 異常
            raise exceptions.AuthenticationFailed()
		
        # 使用 authenticate_credentials 方法驗證令牌的有效性,並返回使用者物件
        user = self.authenticate_credentials(payload)
		
        # 返回使用者物件和校驗成功的token串
        return (user, jwt_value)
	
    # 驗證令牌的有效性並返回與令牌關聯的使用者
    def authenticate_credentials(self, payload):
        """
        Returns an active user that matches the payload's user id and email.
        """
        # 獲取使用者模型
        User = get_user_model()
        # 從JWT令牌的載荷中獲取使用者名稱
        username = jwt_get_username_from_payload(payload)
		
        # 如果載荷中沒有使用者名稱,丟擲 AuthenticationFailed 異常
        if not username:
            msg = _('Invalid payload.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            # 嘗試透過使用者名稱獲取使用者物件
            user = User.objects.get_by_natural_key(username)
        except User.DoesNotExist:
            # 如果使用者不存在,丟擲 AuthenticationFailed 異常
            msg = _('Invalid signature.')
            raise exceptions.AuthenticationFailed(msg)
		
        # 如果使用者被禁用,丟擲 AuthenticationFailed 異常
        if not user.is_active:
            msg = _('User account is disabled.')
            raise exceptions.AuthenticationFailed(msg)
		
        # 返回驗證透過的使用者物件
        return user
  • JSONWebTokenAuthentication
class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
    """
    Clients should authenticate by passing the token key in the "Authorization"
    HTTP header, prepended with the string specified in the setting
    `JWT_AUTH_HEADER_PREFIX`. For example:

        Authorization: JWT eyJhbGciOiAiSFMyNTYiLCAidHlwIj
    """
    
    www_authenticate_realm = 'api'
	
    # 該方法用於獲取JWT令牌的值,它從請求的 Authorization 頭中提取JWT令牌
    def get_jwt_value(self, request):
        # 檢查請求的 Authorization 頭是否存在
        # 然後使用 split() 方法拆分這個內容。這樣,它可以檢查 Authorization 頭是否包含 JWT 令牌。
        auth = get_authorization_header(request).split()
        # 獲取 JWT 令牌的字首(prefix),這個字首由 api_settings.JWT_AUTH_HEADER_PREFIX 指定,通常是 "JWT"。
        auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()
		
        # 如果沒有找到 Authorization 頭
        if not auth:
            # 它會檢查是否啟用了JWT令牌的Cookie支援,並嘗試從Cookie中獲取令牌。
            if api_settings.JWT_AUTH_COOKIE:
                # 如果啟用了,它會嘗試從請求的 Cookie 中獲取 JWT 令牌。
                return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
            return None
		
        # 首先,它會確保 Authorization 頭的第一個元素(通常是字首)與設定中定義的字首相匹配,不區分大小寫。
        if smart_text(auth[0].lower()) != auth_header_prefix:
            return None
		
        # 然後,它檢查 Authorization 頭的長度是否合法。
        # JWT 令牌的格式是 "Bearer {token}",如果頭部長度不等於 2,它會引發 AuthenticationFailed 異常
        if len(auth) == 1:
            msg = _('Invalid Authorization header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid Authorization header. Credentials string '
                    'should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)
		
        # 如果字首匹配,它返回JWT令牌的值
        return auth[1]

    def authenticate_header(self, request):
        """
        Return a string to be used as the value of the `WWW-Authenticate`
        header in a `401 Unauthenticated` response, or `None` if the
        authentication scheme should return `403 Permission Denied` responses.
        """
        # 如果 Authorization 頭存在
        # 它會檢查JWT令牌的字首是否匹配設定中定義的字首(透過 api_settings.JWT_AUTH_HEADER_PREFIX 指定)。
        return '{0} realm="{1}"'.format(api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm)

【八】djangorestframework-simplejwt原始碼分析(自動簽發token)

【1】流程分析

(1)安裝 djangorestframework-simplejwt 模組

pip install djangorestframework-simplejwt

(2)settings檔案註冊APP

from datetime import timedelta

# (1)註冊APP
INSTALLED_APPS = [
	...
    # jwt認證模組註冊
    'rest_framework_simplejwt',
	...
]

# (2)配置登陸校驗類(自定義或使用Django的)
AUTHENTICATION_BACKENDS = [
    'DreamShopApi.utils.common_authentications.Authentice',
]


# (3)配置令牌失效時間
SIMPLE_JWT = {
    # 訪問令牌的有效時間
    "ACCESS_TOKEN_LIFETIME":timedelta(minutes=5),
}

# (4)# djangorestframework-simplejwt 配置令牌資訊
SIMPLE_JWT = {
    # token有效時長(返回的 access 有效時長) - 訪問令牌的有效時間
    'ACCESS_TOKEN_LIFETIME': datetime.timedelta(seconds=10),
    # token重新整理的有效時間(返回的 refresh 有效時長) - 重新整理令牌的有效時間
    'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1),

    # 若為 True ,則重新整理後的refresh_token 有更新的有效時間
    'ROTATE_REFRESH_TOKENS': False,
    # 若為 True , 則重新整理後的token將新增到黑名單中
    'BLACKLIST_AFTER_ROTATION': True,

    # 對稱演算法:HS256 HS384 HS512
    # 非對稱演算法:RSA
    'ALGORITHM': 'HS256',
    #
    'SIGNING_KEY': SECRET_KEY,
    # if signing_key, verifying_key will be ignored
    'VERIFYING_KEY': None,
    #
    'AUDIENCE': None,
    #
    'ISSUER': None,

    # AUTHORIZATION: Bearer < token >
    'AUTH_HEADER_TYPES': ("Bearer"),
    # if HTTP_X_ACCESS_TOKEN,x_ACCESS_TOKEN : Bearer < token >
    "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
    #
    Bearer < token >
    # 使用唯一不變的資料庫欄位,將包含在生成令牌中以標識使用者
    'USER_ID_FIELD': "id",

    "USER_ID_CLAIM": "user_id"
}

(3)路由配置

  • 三個路由都要有
from rest_framework_simplejwt.views import TokenRefreshView,TokenVerifyView

path('login_admin/', AutoTokenLoginView.as_view()),
# 重新整理 token 介面
path('token/refresh/', TokenRefreshView.as_view()),
# 校驗token
path('token/verify/', TokenVerifyView.as_view()),

(4)檢視函式

class AutoTokenLoginView(TokenObtainPairView):

    def post(self, request, *args, **kwargs):
        # 校驗資料 ---- 使用的是 TokenObtainPairView 的序列化類
        serializer = self.get_serializer(data=request.data)
        try:
            # raise_exception : 異常主動丟擲
            # 校驗資料
            serializer.is_valid(raise_exception=True)
        except Exception as e:
            raise InvalidToken(e.args[0])
        result = serializer.validated_data

        id = serializer.user.id
        phone = serializer.user.phone
        email = serializer.user.email
        username = serializer.user.username
        token = result.pop("access")
        re_token = result.pop("refresh")
        return CommonResponse(msg="成功了", id=id, username=username,
                              phone=phone, email=email,
                              token=token, re_token=re_token,
                              status=status.HTTP_200_OK)

【2】原始碼分析

(1)api_settings

from datetime import timedelta
from typing import Any, Dict

from django.conf import settings
from django.test.signals import setting_changed
from django.utils.translation import gettext_lazy as _
from rest_framework.settings import APISettings as _APISettings

from .utils import format_lazy

USER_SETTINGS = getattr(settings, "SIMPLE_JWT", None)

# 預設配置設定(DEFAULTS 字典)
DEFAULTS = {
    # 訪問令牌的生命週期,預設為5分鐘
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),

    # 重新整理令牌的生命週期,預設為1天
    "REFRESH_TOKEN_LIFETIME": timedelta(days=1),

    # 是否輪換重新整理令牌,預設為False
    "ROTATE_REFRESH_TOKENS": False,

    # 輪換後是否將令牌列入黑名單,預設為False
    "BLACKLIST_AFTER_ROTATION": False,

    # 是否更新使用者的最後登入時間,預設為False
    "UPDATE_LAST_LOGIN": False,

    # 令牌的簽名演算法,預設為HS256
    "ALGORITHM": "HS256",

    # 用於簽名的金鑰,預設為Django專案的SECRET_KEY
    "SIGNING_KEY": settings.SECRET_KEY,

    # 用於驗證的金鑰,預設為空
    "VERIFYING_KEY": "",

    # 令牌的受眾(Audience)宣告,預設為None
    "AUDIENCE": None,

    # 令牌的發行者(Issuer)宣告,預設為None
    "ISSUER": None,

    # JSON編碼器,預設為None
    "JSON_ENCODER": None,

    # JSON Web Key (JWK) URL,預設為None
    "JWK_URL": None,

    # 時間偏差,預設為0
    "LEEWAY": 0,

    # 認證頭部的型別,預設為("Bearer",)
    "AUTH_HEADER_TYPES": ("Bearer",),

    # 認證頭部的名稱,預設為"HTTP_AUTHORIZATION"
    "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",

    # 使用者ID欄位,預設為"id"
    "USER_ID_FIELD": "id",

    # 使用者ID宣告,預設為"user_id"
    "USER_ID_CLAIM": "user_id",

    # 使用者認證規則,預設為"rest_framework_simplejwt.authentication.default_user_authentication_rule"
    "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",

    # 認證令牌類,預設為("rest_framework_simplejwt.tokens.AccessToken",)
    "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),

    # 令牌型別宣告,預設為"token_type"
    "TOKEN_TYPE_CLAIM": "token_type",

    # JWT ID宣告,預設為"jti"
    "JTI_CLAIM": "jti",

    # 令牌使用者類,預設為"rest_framework_simplejwt.models.TokenUser"
    "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",

    # 滑動重新整理令牌的重新整理過期時間宣告,預設為"refresh_exp"
    "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",

    # 滑動重新整理令牌的生命週期,預設為5分鐘
    "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),

    # 滑動重新整理令牌的重新整理生命週期,預設為1天
    "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),

    # 普通令牌獲取序列化器,預設為"rest_framework_simplejwt.serializers.TokenObtainPairSerializer"
    "TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",

    # 令牌重新整理序列化器,預設為"rest_framework_simplejwt.serializers.TokenRefreshSerializer"
    "TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",

    # 令牌驗證序列化器,預設為"rest_framework_simplejwt.serializers.TokenVerifySerializer"
    "TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",

    # 令牌加入黑名單序列化器,預設為"rest_framework_simplejwt.serializers.TokenBlacklistSerializer"
    "TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",

    # 是否檢查撤銷令牌,預設為False
    "CHECK_REVOKE_TOKEN": False,

    # 撤銷令牌的宣告,預設為"hash_password"
    "REVOKE_TOKEN_CLAIM": "hash_password",
}


IMPORT_STRINGS = (
    # 認證令牌類
    "AUTH_TOKEN_CLASSES",
    
    # JSON編碼器
    "JSON_ENCODER",
    
    # 令牌使用者類
    "TOKEN_USER_CLASS",
    
    # 使用者認證規則
    "USER_AUTHENTICATION_RULE",
)

REMOVED_SETTINGS = (
    # 認證頭部型別
    "AUTH_HEADER_TYPE",
    
    # 認證令牌類
    "AUTH_TOKEN_CLASS",
    
    # 金鑰
    "SECRET_KEY",
    
    # 令牌後端類
    "TOKEN_BACKEND_CLASS",
)


# APISettings 類繼承了 _APISettings 類,這是 Simple JWT 庫中用於管理配置設定的基類。
class APISettings(_APISettings):  # pragma: no cover
    # __check_user_settings 方法是一個私有方法,用於檢查使用者設定是否包含已被刪除的設定。
    # 它接受一個名為 user_settings 的字典引數,其中包含了使用者在專案中自定義的配置設定。
    def __check_user_settings(self, user_settings: Dict[str, Any]) -> Dict[str, Any]:
        
        # 定義了一個名為 SETTINGS_DOC 的常量,該常量包含了配置文件的連結,使用者可以在文件中檢視可用設定。
        SETTINGS_DOC = "https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html"
		
        # 遍歷名為 REMOVED_SETTINGS 的列表,該列表包含了已被刪除的設定的名稱。
        for setting in REMOVED_SETTINGS:
            # 對於每個已被刪除的設定
            if setting in user_settings:
                # 如果它在使用者設定中存在,就會引發一個 RuntimeError 異常,提供相應的錯誤訊息
                # 告訴使用者該設定已被刪除,並提供文件連結供使用者檢視可用設定。
                raise RuntimeError(
                    format_lazy(
                        _(
                            "The '{}' setting has been removed. Please refer to '{}' for available settings."
                        ),
                        setting,
                        SETTINGS_DOC,
                    )
                )
		# 如果使用者設定中不包含任何已被刪除的設定,方法將返回使用者設定本身,沒有進行任何修改。
        return user_settings


# 建立api_settings物件,用於管理Simple JWT的配置設定
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)

# 定義一個函式reload_api_settings,用於在配置發生更改時重新載入Simple JWT的配置設定
def reload_api_settings(*args, **kwargs) -> None:
    global api_settings

    setting, value = kwargs["setting"], kwargs["value"]

    # 如果配置發生更改,並且更改的配置項是"SIMPLE_JWT"
    if setting == "SIMPLE_JWT":
        # 重新建立api_settings物件,使用新的配置值
        api_settings = APISettings(value, DEFAULTS, IMPORT_STRINGS)

# 使用setting_changed.connect方法連線reload_api_settings函式,以便在配置發生更改時觸發重新載入配置設定的操作
setting_changed.connect(reload_api_settings)

(2)views

class TokenViewBase(generics.GenericAPIView):
    # 這些類屬性定義了檢視的許可權和身份驗證類。
    # 它們都被設定為空,這意味著該檢視不需要任何許可權和身份驗證。
    permission_classes = ()
    authentication_classes = ()
	
    # 定義序列化器類。serializer_class 可以直接設定,或者從 _serializer_class 指定的字串中動態匯入。
    # 如果指定了 serializer_class,將使用它;否則,將嘗試根據 _serializer_class 匯入相應的序列化器類。
    serializer_class = None
    _serializer_class = ""
	
    # 定義了身份驗證領域(Realm),通常用於設定 WWW-Authenticate 頭的值。
    www_authenticate_realm = "api"
	
    # 用於獲取要用於序列化資料的序列化器類。
    # 這個方法允許根據需要靈活地選擇序列化器。
    def get_serializer_class(self) -> Serializer:
        """
        If serializer_class is set, use it directly. Otherwise get the class from settings.
        """
		# 如果 serializer_class 已經設定,它將直接返回;
        if self.serializer_class:
            return self.serializer_class
        try:
            # 否則,它將嘗試從 _serializer_class 匯入相應的序列化器類。
            return import_string(self._serializer_class)
        except ImportError:
            msg = "Could not import serializer '%s'" % self._serializer_class
            raise ImportError(msg)
	
    # 用於返回 WWW-Authenticate 頭的值,通常用於在身份驗證失敗時返回給客戶端的響應頭。
    # 它將 AUTH_HEADER_TYPES 中的第一個型別與 www_authenticate_realm 組合成頭部值。
    def get_authenticate_header(self, request: Request) -> str:
        return '{} realm="{}"'.format(
            AUTH_HEADER_TYPES[0],
            self.www_authenticate_realm,
        )
	
    # 這個方法處理 HTTP POST 請求。
    def post(self, request: Request, *args, **kwargs) -> Response:
        
        # 它首先使用 get_serializer 方法獲取序列化器例項,並將請求資料傳遞給序列化器進行驗證。
        serializer = self.get_serializer(data=request.data)

        try:
            # 如果驗證成功,它將返回一個包含序列化器驗證資料的響應,狀態碼為 HTTP_200_OK。
            serializer.is_valid(raise_exception=True)
        except TokenError as e:
            # 如果序列化器驗證失敗,將引發異常並返回相應的錯誤響應
            raise InvalidToken(e.args[0])

        return Response(serializer.validated_data, status=status.HTTP_200_OK)

# 用於生成訪問令牌(access token)和重新整理令牌(refresh token)的 DRF 檢視
class TokenObtainPairView(TokenViewBase):
    """
    # 這意味著 TokenObtainPairView 類繼承了 TokenViewBase 類的屬性和方法。
    # TokenViewBase 類似乎是一個通用的基礎檢視,用於處理令牌相關的操作。
    Takes a set of user credentials and returns an access and refresh JSON web
    token pair to prove the authentication of those credentials.
    """
	
    # _serializer_class 屬性被設定為 api_settings.TOKEN_OBTAIN_SERIALIZER,這是一個用於生成令牌的序列化器類。
    # 在 TokenObtainPairView 中,這個序列化器將用於處理使用者的憑證(通常是使用者名稱和密碼)並生成訪問令牌和重新整理令牌。
    _serializer_class = api_settings.TOKEN_OBTAIN_SERIALIZER
    	
    # post 方法:
    # TokenObtainPairView 類繼承了 TokenViewBase 類的 post 方法,這個方法用於處理 HTTP POST 請求。
    # 在 post 方法中,它首先使用 _serializer_class 屬性指定的序列化器來驗證使用者的憑證。
    # 如果驗證成功,它將生成並返回訪問令牌和重新整理令牌的 JSON 響應,表示使用者已成功登入並且可以使用這些令牌來進行後續請求。
    # 如果驗證失敗,它將返回相應的錯誤響應,通常包括錯誤訊息。

# token_obtain_pair 檢視函式透過 TokenObtainPairView.as_view() 建立
# 這允許你將該檢視繫結到特定的 URL 路由,以便在需要時呼叫它來獲取令牌
token_obtain_pair = TokenObtainPairView.as_view()


class TokenRefreshView(TokenViewBase):
    """
    # TokenRefreshView 類繼承了 TokenViewBase 類的屬性和方法。TokenViewBase 類似乎是一個通用的基礎檢視,用於處理令牌相關的操作
    Takes a refresh type JSON web token and returns an access type JSON web
    token if the refresh token is valid.
    """
	
    # _serializer_class 屬性被設定為 api_settings.TOKEN_REFRESH_SERIALIZER,這是一個用於重新整理令牌的序列化器類。
    # 在 TokenRefreshView 中,這個序列化器將用於處理重新整理令牌,並生成新的訪問令牌。
    _serializer_class = api_settings.TOKEN_REFRESH_SERIALIZER
    
    # post 方法:
    # TokenRefreshView 類繼承了 TokenViewBase 類的 post 方法,這個方法用於處理 HTTP POST 請求。
    # 在 post 方法中,它首先使用 _serializer_class 屬性指定的序列化器來驗證重新整理令牌。
    # 如果重新整理令牌有效,它將生成並返回一個新的訪問令牌的 JSON 響應,表示使用者已成功重新整理令牌並且可以使用新的訪問令牌來進行後續請求。
    # 如果重新整理令牌無效或過期,它將返回相應的錯誤響應,通常包括錯誤訊息。

# token_refresh 檢視函式透過 TokenRefreshView.as_view() 建立
# 這允許你將該檢視繫結到特定的 URL 路由,以便在需要時呼叫它來重新整理令牌
token_refresh = TokenRefreshView.as_view()


class TokenObtainSlidingView(TokenViewBase):
    """
    # TokenObtainSlidingView 類繼承了 TokenViewBase 類的屬性和方法。
    # TokenViewBase 類似乎是一個通用的基礎檢視,用於處理令牌相關的操作。
    Takes a set of user credentials and returns a sliding JSON web token to
    prove the authentication of those credentials.
    """
	
    # _serializer_class 屬性被設定為 api_settings.SLIDING_TOKEN_OBTAIN_SERIALIZER,這是一個用於生成滑動令牌的序列化器類。
    # 在 TokenObtainSlidingView 中,這個序列化器將用於處理使用者的憑證(通常是使用者名稱和密碼)並生成滑動令牌。
    _serializer_class = api_settings.SLIDING_TOKEN_OBTAIN_SERIALIZER
    	
    # post 方法:
    # TokenObtainSlidingView 類繼承了 TokenViewBase 類的 post 方法,這個方法用於處理 HTTP POST 請求。
    # 在 post 方法中,它首先使用 _serializer_class 屬性指定的序列化器來驗證使用者的憑證。
    # 如果驗證成功,它將生成並返回一個滑動令牌的 JSON 響應,表示使用者已成功登入並且可以使用滑動令牌來進行後續請求。
    # 滑動令牌具有自動重新整理機制,只要使用者保持活動狀態,令牌就會自動延長其有效期。因此,不需要顯式的令牌重新整理操作。

# token_obtain_sliding 檢視函式透過 TokenObtainSlidingView.as_view() 建立
# 這允許你將該檢視繫結到特定的 URL 路由,以便在需要時呼叫它來獲取滑動令牌。
token_obtain_sliding = TokenObtainSlidingView.as_view()


class TokenRefreshSlidingView(TokenViewBase):
    """
    Takes a sliding JSON web token and returns a new, refreshed version if the
    token's refresh period has not expired.
    """
	
    # _serializer_class 屬性被設定為 api_settings.SLIDING_TOKEN_REFRESH_SERIALIZER,這是一個用於重新整理滑動令牌的序列化器類。
    # 在 TokenRefreshSlidingView 中,這個序列化器將用於處理滑動令牌並生成新的滑動令牌。
    _serializer_class = api_settings.SLIDING_TOKEN_REFRESH_SERIALIZER
    	
    # post 方法:
    # TokenRefreshSlidingView 類繼承了 TokenViewBase 類的 post 方法,這個方法用於處理 HTTP POST 請求。
    # 在 post 方法中,它首先使用 _serializer_class 屬性指定的序列化器來驗證滑動令牌。
    # 如果滑動令牌的重新整理期限尚未過期,它將生成並返回一個新的滑動令牌的 JSON 響應,表示使用者的滑動令牌已成功重新整理並且可以使用新的滑動令牌來進行後續請求。
    # 如果滑動令牌的重新整理期限已過期,它將返回相應的錯誤響應,通常包括錯誤訊息。

# token_refresh_sliding 檢視函式透過 TokenRefreshSlidingView.as_view() 建立
# 這允許你將該檢視繫結到特定的 URL 路由,以便在需要時呼叫它來重新整理滑動令牌。
token_refresh_sliding = TokenRefreshSlidingView.as_view()


class TokenVerifyView(TokenViewBase):
    """
    Takes a token and indicates if it is valid.  This view provides no
    information about a token's fitness for a particular use.
    """
	
    # _serializer_class 屬性被設定為 api_settings.TOKEN_VERIFY_SERIALIZER,這是一個用於驗證令牌的序列化器類。
    # 在 TokenVerifyView 中,這個序列化器將用於驗證使用者提供的令牌是否有效。
    _serializer_class = api_settings.TOKEN_VERIFY_SERIALIZER
    
    # post 方法:
    # TokenVerifyView 類繼承了 TokenViewBase 類的 post 方法,這個方法用於處理 HTTP POST 請求。
    # 在 post 方法中,它首先使用 _serializer_class 屬性指定的序列化器來驗證使用者提供的令牌。
    # 如果令牌有效,它將返回一個成功的響應,表示令牌是有效的。
    # 如果令牌無效,它將返回相應的錯誤響應,通常包括錯誤訊息。

# token_verify 檢視函式透過 TokenVerifyView.as_view() 建立
# 這允許你將該檢視繫結到特定的 URL 路由,以便在需要時呼叫它來驗證令牌的有效性。
token_verify = TokenVerifyView.as_view()


class TokenBlacklistView(TokenViewBase):
    """
    Takes a token and blacklists it. Must be used with the
    `rest_framework_simplejwt.token_blacklist` app installed.
    """
	
    # _serializer_class 屬性被設定為 api_settings.TOKEN_BLACKLIST_SERIALIZER
    # 這是一個用於將令牌加入黑名單的序列化器類。
    # 在 TokenBlacklistView 中,這個序列化器將用於處理使用者提供的令牌並將其標記為黑名單中的無效令牌。
    _serializer_class = api_settings.TOKEN_BLACKLIST_SERIALIZER
    
    # post 方法:
    # TokenBlacklistView 類繼承了 TokenViewBase 類的 post 方法,這個方法用於處理 HTTP POST 請求。
    # 在 post 方法中,它首先使用 _serializer_class 屬性指定的序列化器來處理使用者提供的令牌,並將其標記為黑名單中的無效令牌。
    # 加入黑名單的令牌將無法再用於進行有效的身份驗證,從而有效地登出使用者或限制其訪問許可權。

# token_blacklist 檢視函式透過 TokenBlacklistView.as_view() 建立
# 這允許你將該檢視繫結到特定的 URL 路由,以便在需要時呼叫它來將令牌加入黑名單。
token_blacklist = TokenBlacklistView.as_view()

(3)serializers

# 建立一個型別變數AuthUser,用於表示使用者物件,可以是AbstractBaseUser或TokenUser
AuthUser = TypeVar("AuthUser", AbstractBaseUser, TokenUser)

# 如果啟用了TOKEN_BLACKLIST設定,匯入相關模組
if api_settings.BLACKLIST_AFTER_ROTATION:
    from .token_blacklist.models import BlacklistedToken

# 自定義密碼欄位,用於輸入密碼
# 輸入密碼,並將輸入欄位的input_type樣式設定為"password",以確保在前端表單中顯示密碼輸入框。
class PasswordField(serializers.CharField):
    def __init__(self, *args, **kwargs) -> None:
        kwargs.setdefault("style", {})
        # 設定格式為 password
        kwargs["style"]["input_type"] = "password"
        # 設定只寫屬性
        kwargs["write_only"] = True
        # 呼叫父類初始化
        super().__init__(*args, **kwargs)

# TokenObtainSerializer類,用於驗證使用者並獲取令牌
class TokenObtainSerializer(serializers.Serializer):
    # 使用者名稱欄位,預設為使用者模型中的使用者名稱欄位
    username_field = get_user_model().USERNAME_FIELD
    # 令牌類,預設為None
    token_class: Optional[Type[Token]] = None

    # 預設錯誤訊息,用於自定義錯誤訊息
    default_error_messages = {
        "no_active_account": _("No active account found with the given credentials")
    }

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        # 新增使用者名稱欄位和密碼欄位到序列化器中
        self.fields[self.username_field] = serializers.CharField(write_only=True)
        self.fields["password"] = PasswordField()

    # 驗證方法,用於驗證使用者憑證
    def validate(self, attrs: Dict[str, Any]) -> Dict[Any, Any]:
        authenticate_kwargs = {
            self.username_field: attrs[self.username_field],
            "password": attrs["password"],
        }
        try:
            authenticate_kwargs["request"] = self.context["request"]
        except KeyError:
            pass

        self.user = authenticate(**authenticate_kwargs)

        # 檢查使用者是否滿足自定義的使用者驗證規則
        if not api_settings.USER_AUTHENTICATION_RULE(self.user):
            raise exceptions.AuthenticationFailed(
                self.error_messages["no_active_account"],
                "no_active_account",
            )

        return {}

    # 獲取令牌的方法,用於返回使用者的令牌
    @classmethod
    def get_token(cls, user: AuthUser) -> Token:
        return cls.token_class.for_user(user)  # type: ignore

# TokenObtainPairSerializer類,用於獲取訪問令牌和重新整理令牌對
# 這就是為什麼拿到簽發的結果中包含一個 refresh token 和 access token
class TokenObtainPairSerializer(TokenObtainSerializer):
    token_class = RefreshToken

    def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]:
        data = super().validate(attrs)

        refresh = self.get_token(self.user)

        data["refresh"] = str(refresh)
        data["access"] = str(refresh.access_token)

        # 如果配置要求更新最後登入時間,則執行更新操作
        if api_settings.UPDATE_LAST_LOGIN:
            update_last_login(None, self.user)

        return data

# TokenObtainSlidingSerializer類,用於獲取滑動令牌
class TokenObtainSlidingSerializer(TokenObtainSerializer):
    token_class = SlidingToken

    def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]:
        data = super().validate(attrs)

        token = self.get_token(self.user)

        data["token"] = str(token)

        # 如果配置要求更新最後登入時間,則執行更新操作
        if api_settings.UPDATE_LAST_LOGIN:
            update_last_login(None, self.user)

        return data

# TokenRefreshSerializer類,用於重新整理訪問令牌
class TokenRefreshSerializer(serializers.Serializer):
    refresh = serializers.CharField()
    access = serializers.CharField(read_only=True)
    token_class = RefreshToken

    def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]:
        refresh = self.token_class(attrs["refresh"])

        data = {"access": str(refresh.access_token)}

        # 如果配置要求輪換重新整理令牌,執行相應的操作
        if api_settings.ROTATE_REFRESH_TOKENS:
            if api_settings.BLACKLIST_AFTER_ROTATION:
                try:
                    # 嘗試將重新整理令牌加入黑名單
                    refresh.blacklist()
                except AttributeError:
                    # 如果沒有安裝黑名單應用,'blacklist'方法將不會存在
                    pass

            refresh.set_jti()
            refresh.set_exp()
            refresh.set_iat()

            data["refresh"] = str(refresh)

        return data

# TokenRefreshSlidingSerializer類,用於重新整理滑動令牌
class TokenRefreshSlidingSerializer(serializers.Serializer):
    token = serializers.CharField()
    token_class = SlidingToken

    def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]:
        token = self.token_class(attrs["token"])

        # 檢查"refresh_exp"宣告中的時間戳是否已過期
        token.check_exp(api_settings.SLIDING_TOKEN_REFRESH_EXP_CLAIM)

        # 更新"exp"和"iat"宣告
        token.set_exp()
        token.set_iat()

        return {"token": str(token)}

# TokenVerifySerializer類,用於驗證令牌的有效性
class TokenVerifySerializer(serializers.Serializer):
    token = serializers.CharField(write_only=True)

    def validate(self, attrs: Dict[str, None]) -> Dict[Any, Any]:
        token = UntypedToken(attrs["token"])

        # 如果啟用了令牌黑名單,並且在已安裝應用列表中,檢查令牌是否被列入黑名單
        if (
            api_settings.BLACKLIST_AFTER_ROTATION
            and "rest_framework_simplejwt.token_blacklist" in settings.INSTALLED_APPS
        ):
            jti = token.get(api_settings.JTI_CLAIM)
            if BlacklistedToken.objects.filter(token__jti=jti).exists():
                raise ValidationError("Token is blacklisted")

        return {}

# TokenBlacklistSerializer類,用於將令牌列入黑名單
class TokenBlacklistSerializer(serializers.Serializer):
    refresh = serializers.CharField(write_only=True)
    token_class = RefreshToken

    def validate(self, attrs: Dict[str, Any]) -> Dict[Any, Any]:
        refresh = self.token_class(attrs["refresh"])
        try:
            refresh.blacklist()
        except AttributeError:
            pass
        return {}

相關文章