在 Django 开发中,有时需要通过 URL 参数、表单隐藏字段或 Cookie 等方式将数据发送给客户端,然后再接收回来。这时会想到 "这些数据在传输过程中是否被用户修改过呢?" 这个问题。django.core.signing 是为了解决这个问题而提供的强大工具。
该模块提供的是 加密签名 (Cryptographic Signing) 而非加密(Encryption)。
-
加密 (X): 隐藏数据内容。
-
签名 (O): 数据内容可以公开,但 保证数据没有被篡改。
1. 核心用法: dumps() 和 loads()
signing 模块的核心在于 dumps() 和 loads()。将 Python 对象转换为带签名的字符串,或将带签名的字符串验证后恢复为对象。
dumps(): 将对象转换为带签名的字符串
dumps() 接收字典、列表等 JSON 可序列化对象,返回一个 URL 安全的带签名字符串。
from django.core.signing import dumps
# 要签名的数据
user_data = {'user_id': 123, 'role': 'user'}
# 将数据签名为字符串。
signed_data = dumps(user_data)
print(signed_data)
# 输出示例: eyJ1c2VyX2lkIjoxMjMsInJvbGUiOiJ1c2VyIn0:1q51oH:F... (数据:签名)
结果是 [编码数据]:[签名] 形式。前半部分是原始数据的 Base64 编码,因此 任何人都可以解码查看原始内容。 后半部分是由 Django 的 SECRET_KEY 生成的签名(哈希值)。
loads(): 将带签名的字符串恢复为对象 (验证)
loads() 接收由 dumps() 生成的字符串并验证签名。如果签名有效,则返回原对象;如果无效,则抛出 BadSignature 异常。
from django.core.signing import loads, BadSignature
try:
# 将签名的数据恢复为原对象 (验证)
unsigned_data = loads(signed_data)
print(unsigned_data)
# 输出: {'user_id': 123, 'role': 'user'}
except BadSignature:
print("数据被篡改或签名无效。")
# 如果数据改了一个字母呢?
tampered_data = signed_data.replace('user', 'admin')
try:
loads(tampered_data)
except BadSignature:
print("伪造的数据会引发 BadSignature 异常。")
必须始终使用 try...except BadSignature 结构。
2. 核心特点:数据存储在哪里?
django.core.signing 最大的特点是 "无状态 (Stateless)"。
通过 dumps() 生成的字符串不会存储在服务器的 数据库或缓存中。 所有信息(数据和签名)都包含在该带签名字符串中。
服务器仅将该字符串传递给客户端(通过 URL、Cookie、表单字段等),当客户端再次提交该值时,仅用 SECRET_KEY 即时验证其有效性。这使得服务器完全不使用存储空间,极其轻便高效。
3. 设置有效期:max_age 的秘密
"数据没有存储在服务器上,那怎么能设置有效期呢?"
max_age 选项是在 dumps() 时 将时间戳(时间信息)包含在数据的签名中。
调用 loads() 时传递 max_age 参数(以秒为单位),在验证签名后将检查内置的时间戳。
-
计算当前时间和时间戳之间的差值。
-
如果这个差值超过
max_age,即使签名未被篡改也会抛出SignatureExpired异常。
from django.core.signing import dumps, loads, SignatureExpired, BadSignature
import time
# 1. 生成签名(此时的时间被记录)
signed_data = dumps({'user_id': 456})
# 2. 检测 10 秒有效期(立即) -> 成功
try:
data = loads(signed_data, max_age=10)
print(f"验证成功: {data}")
except SignatureExpired:
print("签名已过期。")
except BadSignature:
print("签名无效。")
# 3. 等待 5 秒
time.sleep(5)
# 4. 检测 3 秒有效期(已经超过 5 秒) -> 失败
try:
data = loads(signed_data, max_age=3)
print(f"验证成功: {data}")
except SignatureExpired:
print("签名已过期。 (max_age=3)")
except BadSignature:
print("签名无效。")
这同样是 不在服务器上存放过期时间,而是利用包含在签名中的时间戳的 无状态 方式。
4. 重要!注意事项及使用技巧
-
SECRET_KEY是生命。
所有签名机制都是基于 settings.py 的 SECRET_KEY 运行的。如果这个密钥被泄露,任何人都可以生成有效的签名,因此绝对不能外泄。(请勿上传到 Git!)
-
请记住这不是加密。
带签名数据的前半部分(Base64)任何人都可以轻易解码以查看原始内容。绝不要将密码、个人信息等敏感数据直接放入 dumps() 中。(例如:user_id 是可以的,但 user_password 不可以。)
-
使用 salt 区分签名。
在使用不同用途的签名时,请使用 salt 参数。如果 salt 不同,即使数据相同,签名结果也会完全不同。
# 根据用途使用不同的 salt
pw_reset_token = dumps(user.pk, salt='password-reset')
unsubscribe_link = dumps(user.pk, salt='unsubscribe')
这样可以防止将“退订链接”用作“密码重置”的攻击。
5. 主要应用案例
-
密码重置 URL: 将用户 ID 和时间戳签名后通过电子邮件发送(不需要在数据库中保存临时令牌)
-
电子邮件认证链接: 新用户注册时的电子邮件认证链接
-
安全的
nextURL: 防止登录后重定向到恶意网站的?next=/private/URL 被更改 -
临时下载链接: 生成在特定时间内(
max_age)有效的文件访问 URL -
多步骤表单 (Form Wizard): 传递前一步的表单数据到下一步时防止数据篡改
目前没有评论。