【14.0】DRF之JWT

【一】引入(cookie/session/token)

  • 详见博客

【4.0】基础串联之CookieSessionToken - Chimengmeng - 博客园 (cnblogs.com)

cookie、session和token都是用于在网络应用中进行身份验证和状态管理的机制。

【1】详解

  • Cookie(HTTP Cookie):

    • Cookie是服务器发送到用户浏览器并保存在用户本地机器上的一小段数据。

    • 它主要用于跟踪用户的会话状态、存储用户偏好设置以及提供个性化的服务。

    • 当用户访问同一网站时,浏览器会自动将相应的Cookie信息包含在HTTP请求头中发送给服务器。

    • 服务器可以根据这些信息来判断用户的身份或状态,并做出相应的响应。

  • Session(会话):

    • Session是一种服务器端的机制,用于跟踪用户的会话状态。

    • 当用户首次访问网站时,服务器为该用户创建一个唯一的Session ID,并将其存储在服务器内存或数据库中。

    • 同时,在用户的浏览器中存储一个包含Session ID的Cookie。

    • 之后的每个请求都会带有这个Session ID,服务器通过它来识别用户,并可以在会话期间保存用户的相关数据。

    • Session更安全,因为Session数据保存在服务器端,用户无法直接修改。

  • Token(令牌):

    • Token是一种在身份验证和授权方面更加灵活和安全的机制。
    • 它是一个包含有关用户身份和权限的加密字符串,生成和验证均在服务器端进行。
    • 当用户登录后,服务器会生成一个Token并返回给客户端。
    • 客户端将此Token存储,并在每次请求时将其包含在请求头中。
    • 服务器根据Token验证用户的身份和权限,并做出相应的响应。
    • Token可以是无状态的,服务器端不需要存储Token相关的信息,因此有效减轻了服务器的存储压力。

【2】发展史:

  • Cookie最早由网景公司的程序员Lou Montulli于1994年提出,目的是实现在网站间共享用户状态的机制。

    • 随着互联网的发展,Cookie被广泛使用,并逐渐演化成为Web开发中不可或缺的一部分。
  • Session则是作为对Cookie的补充而出现的。

    • 由于Cookie存在某些安全和存储上的限制,Session被引入以解决这些问题。
    • Session的概念首次由Randy Waki和Haden King于2000年在一篇名为《Session Tracking Mechanism》的研究论文中提出。
  • Token作为一种更加灵活和安全的身份验证机制,近年来得到越来越广泛的应用。

    • 它主要受益于移动应用的兴起和各种Web API的普及。
    • Token的概念并没有一个明确的起源,但随着JWT(JSON Web Token)的出现,Token得到了更大范围的应用。
    • JWT是一种基于JSON的开放标准(RFC 7519),用于在各方之间安全地传输信息,并可以被验证和信任。

【二】base64编码解码

详见博客:Day 29 29.3 base64编码 - Chimengmeng - 博客园 (cnblogs.com)

  • Base64编码是一种将二进制数据以文本形式表示的编码方式。
  • 它将3个字节的二进制数据分割为4个6位的片段,然后将每个6位片段映射到一个可打印字符。
  • Base64编码主要用于在传输过程中保存二进制数据,例如在电子邮件中传输二进制附件或在网页中嵌入图像。

【1】详解

  • Base64编码原理:

    • 将需要编码的数据拆分成3个字节一组(24位)。
    • 将这24位按照6位一组进行分割,得到4组6位数字。
    • 将每个6位数字转换成对应的ASCII码表示的可打印字符,最后得到4个字符。
    • 如果数据不足3个字节,则进行补齐处理。
  • Base64编码表:

    • Base64编码使用64个字符来表示6位数字(0-9,A-Z,a-z,+和/)。

    • 由于URL中对特殊字符有限制,所以有时会使用URL安全的Base64编码,将"+"和"/"替换为"-"和"_"。

  • Base64编码的应用案例:

    • 假设有一个包含如下二进制数据的字符串: "Man"
      • 首先,将字符串 "Man" 转换为ASCII码表示的二进制数据: 77, 97, 110。
      • 然后,将这三个字节合并为一个24位的二进制数:01001101 01100001 01101110。
      • 接下来,将这24位的二进制数按照6位一组拆分组成四组:010011 010110 000101 101110。
      • 最后,将每组6位的数字转换为对应的Base64字符:S, 2, F, u。
      • 结果就是编码后的Base64字符串:"S2Fu"

【2】案例演示

(1)基础版

import base64

def base64_encode(data):
    # 对数据进行Base64编码
    encoded_data = base64.b64encode(data)
    return encoded_data

def base64_decode(encoded_data):
    # 对Base64编码的数据进行解码
    decoded_data = base64.b64decode(encoded_data)
    return decoded_data

# 示例数据
original_data = b"Hello, World!"

# 编码示例
encoded_data = base64_encode(original_data)
print("Base64 编码结果:", encoded_data)

# 解码示例
decoded_data = base64_decode(encoded_data)
print("Base64 解码结果:", decoded_data)

解释:

  • 首先

    • 我们导入了Python的base64库,它提供了Base64编码和解码的功能。
  • base64_encode 函数接受一个二进制数据作为输入

    • 使用 base64.b64encode() 方法对数据进行Base64编码。

    • 编码后的结果为字节串类型(bytes)。

  • base64_decode 函数接受一个Base64编码后的字节串数据作为输入

    • 使用 base64.b64decode() 方法对数据进行解码

    • 将其还原为原始的二进制数据。

  • 在示例中,我们使用字符串 "Hello, World!" 进行演示。

    • 在进行编码时,原始数据需要以字节串(bytes)的形式传入

    • 因此我们使用 b"Hello, World!" 将字符串转换为字节串。

  • 然后

    • 我们分别调用 base64_encodebase64_decode 函数对原始数据进行编码和解码。
  • 最后

    • 打印编码和解码的结果。
  • 请注意

    • 编码后的数据为字节串型
    • 解码后的数据与原始数据相同,也是字节串型。

(2)迭代(packunpack打包数据)

  • 使用Python的struct模块中的packunpack方法来完善
import base64
import struct

def base64_encode(data):
    # 对数据进行Base64编码
    encoded_data = base64.b64encode(data)
    return encoded_data

def base64_decode(encoded_data):
    # 对Base64编码的数据进行解码
    decoded_data = base64.b64decode(encoded_data)
    return decoded_data

def pack_data(data):
    # 将数据打包为二进制格式
    packed_data = struct.pack("!I", len(data)) + data
    return packed_data

def unpack_data(packed_data):
    # 解包并获取原始数据
    length = struct.unpack("!I", packed_data[:4])[0]
    data = packed_data[4:4+length]
    return data

# 示例数据
original_data = b"Hello, World!"

# 编码示例
encoded_data = base64_encode(original_data)
packed_encoded_data = pack_data(encoded_data)
print("Base64 编码并打包后的结果:", packed_encoded_data)

# 解包示例
unpacked_encoded_data = unpack_data(packed_encoded_data)
decoded_data = base64_decode(unpacked_encoded_data)
print("解包并解码后的结果:", decoded_data)

解释:

  • 首先

    • 我们仍然使用之前的base64_encodebase64_decode函数对数据进行Base64编码和解码。
  • 现在,我们引入了pack_dataunpack_data两个新的函数。

    • pack_data函数接受一个字节串(bytes)作为输入

      • 使用struct.pack("!I", len(data))将数据长度打包为4个字节的二进制格式,并与原始数据拼接起来,形成打包后的数据。
    • unpack_data函数接受打包后的数据作为输入

      • 使用struct.unpack("!I", packed_data[:4])[0]解包前4个字节的数据长度,并根据长度获取原始数据。
  • 在示例中,我们先将原始数据进行Base64编码,然后将编码结果打包,得到packed_encoded_data

  • 接下来,展示了如何使用unpack_data函数将打包后的数据解包,并使用base64_decode函数对解包后的数据进行解码,得到最终的解码结果。

(4)迭代(padunpad填充)

  • 通过使用Python的Padding模块中的padunpad方法来完善
import base64
from Crypto.Cipher import AES
from Crypto.Util import Padding

def base64_encode(data):
    # 对数据进行Base64编码
    encoded_data = base64.b64encode(data)
    return encoded_data

def base64_decode(encoded_data):
    # 对Base64编码的数据进行解码
    decoded_data = base64.b64decode(encoded_data)
    return decoded_data

def pad_data(data):
    # 使用PKCS7填充数据
    padded_data = Padding.pad(data, AES.block_size)
    return padded_data

def unpad_data(padded_data):
    # 去除PKCS7填充
    unpadded_data = Padding.unpad(padded_data, AES.block_size)
    return unpadded_data

# 示例数据
original_data = b"Hello, World!"

# 编码示例
encoded_data = base64_encode(original_data)
padded_encoded_data = pad_data(encoded_data)
print("Base64 编码并进行填充后的结果:", padded_encoded_data)

# 解码示例
unpadded_encoded_data = unpad_data(padded_encoded_data)
decoded_data = base64_decode(unpadded_encoded_data)
print("去除填充并解码后的结果:", decoded_data)

解释:

  • 首先,我们仍然使用之前的base64_encodebase64_decode函数对数据进行Base64编码和解码。

  • 现在,我们引入了pad_dataunpad_data两个新的函数。pad_data函数接受一个字节串(bytes)作为输入,使用Padding模块的pad方法对数据进行PKCS7填充,并得到填充后的数据。

  • unpad_data函数接受填充后的数据作为输入,使用Padding模块的unpad方法去除PKCS7填充,并返回原始数据。

  • 在示例中,我们先将原始数据进行Base64编码,然后对编码结果进行填充,得到padded_encoded_data

  • 接下来,展示了如何使用unpad_data函数将填充后的数据去除填充,并使用base64_decode函数对解除填充后的数据进行解码,得到最终的解码结果。

【3】总结:

  • Base64编码可以将二进制数据转换为文本形式,方便通过文本传输和展示。
  • 但需要注意,Base64编码并不会增加数据安全性,而且会使数据变大约1.33倍。
  • 它主要用于在各种场景中,如网络传输、数据存储和数据展示等。

【三】JWT认证和Session认证的区别

  • 基于session的认证

  • 基于session认证下的集群部署

  • 基于JWT认证

  • 基于JWT认证下的服务器集群部署

JWT(JSON Web Token)认证和Session认证是两种常见的身份认证机制。它们有一些重要的区别:

【1】会话状态:

  • Session认证:
    • 在Session认证中,服务器会为每个用户创建一个唯一的会话标识符,并将其存储在服务器端,通常在内存或数据库中。
    • 客户端在认证成功后会收到一个Session ID,该ID需要在每次请求中通过Cookie或者其他方式发送给服务器验证。
    • 服务器根据Session ID来验证用户的身份并维护会话状态。
  • JWT认证:
    • 在JWT认证中,服务器不需要在服务器端存储任何会话信息。
    • 认证成功后,服务器生成一个JSON Web Token,并将其发送给客户端保存。
    • 客户端随后会在每次请求中通过HTTP头部将该Token发送给服务器进行验证。
    • 服务器使用密钥对Token进行验证并解析其中的信息来确认用户的身份。

【2】无状态性:

  • Session认证:
    • 由于服务器需要存储会话信息,Session认证机制被称为有状态(stateful)认证。
    • 每次请求都需要服务器在会话存储中查找相关信息,导致服务器的负载较高。
  • JWT认证:
    • JWT认证机制是无状态(stateless)认证,因为服务器不需要在服务器端存储任何会话信息。
    • 服务器只需要对接收到的令牌进行验证即可,这样减轻了服务器负担。

【3】扩展性和跨域支持:

  • Session认证:
    • 对于大规模分布式系统或者跨域认证,Session认证需要额外的配置和管理。
    • 如果用户登录了一个服务器,但又要访问另一个服务器,那么会话信息无法共享,需要特殊处理来实现跨域认证。
  • JWT认证:
    • 由于JWT是基于Token的,它可以被跨域传送并在不同服务之间共享。
    • 这使得JWT更容易在分布式系统中实现认证,并且适用于跨域的场景。

【4】安全性:

  • Session认证:
    • 由于服务器存储会话信息,一旦服务器被攻破或者会话信息被窃取,可能会导致安全问题。
  • JWT认证:
    • JWT通过数字签名的方式保证了数据的完整性和真实性。
    • 只有使用密钥签名的Token才能被服务器接受,因此更难以伪造或篡改。
    • 然而,如果令牌在传输过程中被拦截,则攻击者可以解码其中的信息,因此在使用JWT时需要采取适当的安全措施,如使用HTTPS等。

【四】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是一种灵活且安全的身份认证和授权机制,可以用于构建分布式系统和跨域认证场景。但在使用时需注意安全性和令牌的大小。

【七】Django + JWT 快速使用

  • 使用第三方模块

    django-rest-framework-jwt:
    
    pip3 install djangorestframework-jwt
    
    https://github.com/jpadilla/django-rest-framework-jwt
    
    djangorestframework-simplejwt:
    
    pip3 install djangorestframework-simplejwt
    
    https://github.com/jazzband/djangorestframework-simplejwt
    
  • 我们可以自己封装

    https://gitee.com/liuqingzheng/rbac_manager/tree/master/libs/lqz_jwt
    

【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串

【八】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】创建用户表

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"
    }
    

【九】Django + JWT 自定义认证类

  • drf的认证类定义方式
  • 在认证类中,自己写逻辑
# -*-coding: Utf-8 -*-
# @File : jwt_authentication .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/31
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": "解码失败"
    }
    

【十】Django + JWT 的签发源码分析

# 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
  • JSONWebTokenAPIViewserializer_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_classesauthentication_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和用户信息的字典。如果验证或认证失败,则抛出相应的异常。

【十一】Django + JWT 的认证源码分析

# from rest_framework_jwt.authentication import JSONWebTokenAuthentication
  • 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'

    def get_jwt_value(self, request):
        # auth=['jwt','token串']
        auth = get_authorization_header(request).split()
        auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()

        if not auth:
            if api_settings.JWT_AUTH_COOKIE:
                return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
            return None

        if smart_text(auth[0].lower()) != auth_header_prefix:
            return None

        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)

        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.
        """
        return '{0} realm="{1}"'.format(api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm)
  • get_jwt_value(self, request):
    • 这个方法用于从请求中获取JWT值。
    • 首先,通过get_authorization_header(request)获取请求的Authorization header,并使用split()方法分割字符串,得到一个包含两个元素的列表,分别是认证类型和token。
    • 然后,检查认证类型是否与设置中的JWT_AUTH_HEADER_PREFIX相匹配,如果不匹配则返回None
    • 接着,判断列表的长度,如果长度为1,则说明没有提供credentials,抛出AuthenticationFailed异常。
    • 如果长度大于2,则说明credentials中包含了空格,也不符合格式要求,同样抛出异常。
    • 最后,返回提取到的token。
  • authenticate_header(self, request):
    • 这个方法用于指定在401 Unauthenticated响应中WWW-Authenticate头部的值。
    • 返回的字符串格式为"JWT_AUTH_HEADER_PREFIX realm="api"
  • 父类BaseJSONWebTokenAuthentication中的authenticate方法
class BaseJSONWebTokenAuthentication(BaseAuthentication):
    """
    Token based authentication using the JSON Web Token standard.
    """
	# 入口:重写了 authenticate 方法
    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`.
        """
        # 前端传入的 token,前端传入的样子是  jwt token串
        jwt_value = self.get_jwt_value(request)
        # 如果前端没有传入,则返回None,request.user中就没有当前登录用户
        # 如果前端没有携带token,也能进入到视图类的方法中执行,控制不住登录用户
        # 所以加了个权限类,来做控制
        if jwt_value is None:
            return None

        try:
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            msg = _('Signature has expired.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = _('Error decoding signature.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()
		
        # 通过payload拿到当前登录用户
        user = self.authenticate_credentials(payload)

        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()
        username = jwt_get_username_from_payload(payload)

        if not username:
            msg = _('Invalid payload.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            user = User.objects.get_by_natural_key(username)
        except User.DoesNotExist:
            msg = _('Invalid signature.')
            raise exceptions.AuthenticationFailed(msg)

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

        return user
  • 上述代码是一个基于JSON Web Token(JWT)的身份验证类BaseJSONWebTokenAuthentication,它继承自Django框架中的BaseAuthentication类。
    • 该类是用于实现基于JWT标准的身份验证。
  • 这个类中最重要的方法是authenticate方法
    • 它接受一个request对象作为参数,并返回一个包含User和token的元组
    • 如果提供了有效的签名进行了JWT身份验证。否则,返回None
  • authenticate方法中
    • 首先通过调用get_jwt_value方法从请求中获取jwt_value,该方法从请求头或查询字符串中获取jwt token值。
    • 如果没有传入jwt token,则返回None
  • 接下来
    • authenticate方法尝试使用jwt_decode_handler方法解码jwt token。
    • 如果解码失败,可能是由于过期、无效的签名等原因,将抛出exceptions.AuthenticationFailed异常。
  • 然后
    • 通过调用authenticate_credentials方法,传入解码后的payload来获取与当前用户匹配的活跃用户。
    • 这里调用了Django自带的get_user_model方法获取用户模型并执行查询。
    • 如果没有找到指定的用户,将抛出exceptions.AuthenticationFailed异常。
  • 最后
    • 将获取到的用户和jwt token一起返回。
    • 这样,在视图中使用这个身份验证类时,可以通过访问request.user属性获得当前登录的用户对象。
  • 需要注意的是
    • 如果前端没有传入jwt token,即jwt_valueNone,则直接返回None,在后续的视图处理中无法控制登录用户。
    • 因此,在使用该身份验证类时,应结合权限类来进行进一步控制和限制。
  • jwt_get_username_from_payload(payload)
def authenticate_credentials(self, payload):
    """
    Returns an active user that matches the payload's user id and email.
    """
    User = get_user_model()
    username = jwt_get_username_from_payload(payload)

    if not username:
        msg = _('Invalid payload.')
        raise exceptions.AuthenticationFailed(msg)

    try:
        # 查询当前登录用户
        user = User.objects.get_by_natural_key(username)
    except User.DoesNotExist:
        msg = _('Invalid signature.')
        raise exceptions.AuthenticationFailed(msg)

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

    return user
  • 上述代码是一个身份验证函数authenticate_credentials,用于验证JWT (JSON Web Token) 的凭证信息。
  • 以下是对代码功能的详解:
    • 首先,通过调用get_user_model()函数获取用户模型(User Model)。
    • 通过调用jwt_get_username_from_payload(payload)函数从JWT的负载中获取用户名。
    • 如果未能成功获取到用户名,会抛出exceptions.AuthenticationFailed异常,并返回一个"Invalid payload."的错误消息。
    • 接下来,使用User.objects.get_by_natural_key(username)方法查询与获取到的用户名匹配的活跃用户。
    • 如果找不到对应的用户,则会抛出User.DoesNotExist异常,并返回一个"Invalid signature."的错误消息。
    • 如果查询到的用户不是处于激活状态,则会抛出exceptions.AuthenticationFailed异常,并返回一个"User account is disabled."的错误消息。
    • 如果用户验证通过,则返回验证成功的用户对象。
  • 这段代码的作用是根据JWT的payload中的用户信息进行用户身份验证,返回对应的已验证和激活的用户对象。
  • 如果用户不携带token,也能认证通过
    • 所以我们必须加个权限类来限制
class IsAuthenticated(BasePermission):
    def has_permission(self, request, view):
        return bool(request.user and request.user.is_authenticated)
  • 上述代码是一个用于限制用户权限的权限类IsAuthenticated
  • 用于确保用户必须携带有效的身份认证信息(如JWT token)才能通过权限验证。
  • 以下是对代码功能的详解:
    • IsAuth}enticated是从Django Rest Framework中的BasePermission类继承而来的自定义权限类,用于验证用户是否已经通过了认证和授权。
    • has_permission是权限类的方法,用于判断请求是否有权限执行相应的操作。
    • has_permission方法中,首先通过request.user获取当前请求的用户对象。
    • 然后使用request.user.is_authenticated判断用户是否已经进行了身份认证,即用户是否处于登录状态。
    • 如果request.user存在且request.user.is_authenticated为True,则返回True,表示用户具有权限。
    • 如果request.user不存在或request.user.is_authenticated为False,则返回False,表示用户没有权限。
    • 在视图中使用该权限类,在需要限制权限的接口上添加permission_classes = [IsAuthenticated]即可实现只允许已认证用户访问的控制。
  • 该权限类的作用是在用户进行请求时,检查用户是否已经通过了身份认证
    • 如果认证通过则返回True,否则返回False,从而实现对未认证用户的权限限制。
posted @ 2023-07-31 21:49  Chimengmeng  阅读(58)  评论(0编辑  收藏  举报