在 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 参数(以秒为单位),则在签名验证后检查内置的时间戳。

  1. 计算当前时间与时间戳的差值。

  2. 如果这个差值超过 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. 重要!注意事项及使用提示

  1. SECRET_KEY 是生命线。

    所有签名机制都是基于 settings.py 中的 SECRET_KEY 操作的。一旦该密钥被泄露,任何人都可以生成有效的签名,因此绝不能外泄。 (不要上传到 Git!)

  2. 请注意这不是加密。

    签名数据的前部分(Base64)可以被任何人轻易解码以查看原始内容。绝不要将密码、个人信息等敏感数据直接放入 dumps() 中。(例如:user_id 可以,但 user_password 不可以。)

  3. 使用 salt 分离签名。

    当对不同用途使用签名时,请使用 salt 参数。不同的 salt 会令相同的数据生成完全不同的签名结果。

# 根据用途使用不同的 salt
pw_reset_token = dumps(user.pk, salt='password-reset')
unsubscribe_link = dumps(user.pk, salt='unsubscribe')
这样可以防止“注销链接”的签名被复用于“密码重置”等攻击。

5. 主要使用案例

  • 密码重置 URL: 将用户的 ID 和时间戳进行签名,发送到电子邮件中(无需在数据库中存储临时令牌)

  • 电子邮件认证链接: 新用户注册时的电子邮件认证链接

  • 安全的 next URL: 防止登录后重定向 ?next=/private/ URL 被更改为恶意网站

  • 临时下载链接: 创建在特定时间(max_age)内有效的文件访问 URL

  • 多步骤表单 (Form Wizard): 在将前一步表单数据传递到下一步时防止数据篡改