【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编码解码
- 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"
 
 
- 假设有一个包含如下二进制数据的字符串: "Man"
【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_encode和base64_decode函数对原始数据进行编码和解码。
 
- 我们分别调用 
- 
最后 - 打印编码和解码的结果。
 
- 
请注意 - 编码后的数据为字节串型
- 解码后的数据与原始数据相同,也是字节串型。
 
(2)迭代(pack和unpack打包数据)
- 使用Python的struct模块中的pack和unpack方法来完善
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_encode和base64_decode函数对数据进行Base64编码和解码。
 
- 我们仍然使用之前的
- 
现在,我们引入了 pack_data和unpack_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)迭代(pad和unpad填充)
- 通过使用Python的Padding模块中的pad和unpad方法来完善
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_encode和base64_decode函数对数据进行Base64编码和解码。
- 
现在,我们引入了 pad_data和unpad_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工作流程如下:
- 用户向服务器发送登录请求,提供用户名和密码。
- 服务器验证用户凭证,如果通过验证,生成JWT并将其返回给客户端。
- 客户端收到JWT后,保存在本地(通常在本地存储或者Cookie中)。
- 客户端每次访问受限资源时,在请求头中附带JWT。
- 服务器接收到请求后,使用相同的密钥解析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-jwtdjangorestframework-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。
 
- 在URL路由中添加登录接口路径,使用
- 基于 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
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方法:
- 此方法返回给序列化器类的额外上下文信息。
- 默认情况下,提供了
request和view信息作为上下文。
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_value为None,则直接返回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,从而实现对未认证用户的权限限制。
本文来自博客园,作者:Chimengmeng,转载请注明原文链接:https://www.cnblogs.com/dream-ze/p/17594603.html

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号