【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-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。
- 在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