加密签名

Web 应用安全的黄金法则是永远不要信任来自不可信来源的数据。有时,通过不可信渠道传递数据可能会很有用。通过加密签名的值可以通过不可信渠道传递,因为我们可以安全地知道任何篡改都将被检测到。

Django 提供了用于签名值的低级 API 和用于设置和读取签名 cookie 的高级 API,签名在 web 应用程序中最常见的用途之一就是签名 cookie。

你可能还发现签名对以下方面很有用:

  • 生成“找回我的账户”URL 以发送给丢失密码的用户。
  • 确认存储在表单隐藏字段中的数据未被篡改。
  • 生成一次性的秘密 URL,允许临时访问受保护的资源,例如用户付费下载的文件。

保护 SECRET_KEYSECRET_KEY_FALLBACKS

当你使用 startproject 创建一个新的Django项目时,settings.py 文件会自动生成,并随机得到一个 SECRET_KEY 值。这个值是保证签名数据安全的关键——你必须保证这个值的安全,否则攻击者可以用它来生成自己的签名值。

SECRET_KEY_FALLBACKS 可以用于轮换密钥。这些值不会用于签署数据,但如果指定了,它们将用于验证签署的数据,并必须保持安全。

使用低级 API

Django 的签名方法位于 django.core.signing 模块中。要签署一个值,首先实例化一个 Signer 实例:

  1. >>> from django.core.signing import Signer
  2. >>> signer = Signer()
  3. >>> value = signer.sign("My string")
  4. >>> value
  5. 'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'

签名被追加到字符串的末尾,跟在冒号后面。你可以使用 unsign 方法检索原始值:

  1. >>> original = signer.unsign(value)
  2. >>> original
  3. 'My string'

如果你将一个非字符串值传递给 sign,该值将在签名之前被强制转换为字符串,而 unsign 的结果将给你该字符串值:

  1. >>> signed = signer.sign(2.5)
  2. >>> original = signer.unsign(signed)
  3. >>> original
  4. '2.5'

如果你希望保护一个列表、元组或字典,可以使用 sign_object()unsign_object() 方法来实现:

  1. >>> signed_obj = signer.sign_object({"message": "Hello!"})
  2. >>> signed_obj
  3. 'eyJtZXNzYWdlIjoiSGVsbG8hIn0:Xdc-mOFDjs22KsQAqfVfi8PQSPdo3ckWJxPWwQOFhR4'
  4. >>> obj = signer.unsign_object(signed_obj)
  5. >>> obj
  6. {'message': 'Hello!'}

详见 保护复杂的数据结构

如果签名或值已经被以任何方式更改,将会引发 django.core.signing.BadSignature 异常:

  1. >>> from django.core import signing
  2. >>> value += "m"
  3. >>> try:
  4. ... original = signer.unsign(value)
  5. ... except signing.BadSignature:
  6. ... print("Tampering detected!")
  7. ...

默认情况下,Signer 类使用 SECRET_KEY 设置来生成签名。你可以通过将不同的秘钥传递给 Signer 构造函数来使用不同的秘钥:

  1. >>> signer = Signer(key="my-other-secret")
  2. >>> value = signer.sign("My string")
  3. >>> value
  4. 'My string:EkfQJafvGyiofrdGnuthdxImIJw'

class Signer(*, key=None, sep=’:’, salt=None, algorithm=None, fallback_keys=None)[源代码]

返回一个使用 key 生成签名并使用 sep 分隔值的签名器。sep 不能包含在 URL 安全的 base64 字母表 中。这个字母表包含字母数字字符、连字符和下划线。algorithm 必须是 hashlib 支持的算法,默认值为 'sha256'fallback_keys 是一个用于验证签名数据的附加值列表,默认为 SECRET_KEY_FALLBACKS

使用 salt 参数

如果你不希望每次出现特定字符串时都具有相同的签名哈希值,可以使用 Signer 类的可选参数 salt。使用盐会为签名哈希函数提供盐和你的 SECRET_KEY

  1. >>> signer = Signer()
  2. >>> signer.sign("My string")
  3. 'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
  4. >>> signer.sign_object({"message": "Hello!"})
  5. 'eyJtZXNzYWdlIjoiSGVsbG8hIn0:Xdc-mOFDjs22KsQAqfVfi8PQSPdo3ckWJxPWwQOFhR4'
  6. >>> signer = Signer(salt="extra")
  7. >>> signer.sign("My string")
  8. 'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
  9. >>> signer.unsign("My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw")
  10. 'My string'
  11. >>> signer.sign_object({"message": "Hello!"})
  12. 'eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I'
  13. >>> signer.unsign_object(
  14. ... "eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I"
  15. ... )
  16. {'message': 'Hello!'}

以这种方式使用盐,会将不同的签名放入不同的命名空间。 来自一个命名空间的签名(一个特定的盐值)不能用于验证在使用不同盐值设置的不同命名空间中的同一明文字符串。这样做的结果是防止攻击者将代码中某个地方生成的签名字符串作为输入,输入到使用不同盐值生成(和验证)签名的另一段代码中。

与你的 SECRET_KEY 不同,你的盐参数不需要保密。

验证时间戳值

TimestampSignerSigner 的子类,它会在值后附加一个带签名的时间戳。这允许你确认一个签名值是否在指定的时间段内创建:

  1. >>> from datetime import timedelta
  2. >>> from django.core.signing import TimestampSigner
  3. >>> signer = TimestampSigner()
  4. >>> value = signer.sign("hello")
  5. >>> value
  6. 'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
  7. >>> signer.unsign(value)
  8. 'hello'
  9. >>> signer.unsign(value, max_age=10)
  10. SignatureExpired: Signature age 15.5289158821 > 10 seconds
  11. >>> signer.unsign(value, max_age=20)
  12. 'hello'
  13. >>> signer.unsign(value, max_age=timedelta(seconds=20))
  14. 'hello'

class TimestampSigner(*, key=None, sep=’:’, salt=None, algorithm=’sha256’)[源代码]

  • sign(value)[源代码]

    签名 value 并附加当前时间戳。

  • unsign(value, max_age=None)[源代码]

    检查 value 是否在 max_age 秒前被签署,否则引发 SignatureExpiredmax_age 参数可以接受一个整数或一个 datetime.timedelta 对象。

  • sign_object(obj, serializer=JSONSerializer, compress=False)

    对复杂的数据结构(例如列表、元组或字典)进行编码,可选地压缩数据,追加当前时间戳,并对其进行签名。

  • unsign_object(signed_obj, serializer=JSONSerializer, max_age=None)

    检查 signed_obj 是否在不超过 max_age 秒之前签名,否则会引发 SignatureExpired 异常。max_age 参数可以接受整数或 datetime.timedelta 对象。

保护复杂的数据结构

如果你希望保护一个列表、元组或字典,可以使用 Signer.sign_object()unsign_object() 方法,或者使用签名模块的 dumps()loads() 函数(它们是 TimestampSigner(salt='django.core.signing').sign_object()/unsign_object() 的快捷方式)。这些方法在底层使用了 JSON 序列化。JSON 确保即使你的 SECRET_KEY 被窃取,攻击者也无法通过利用 pickle 格式来执行任意命令:

  1. >>> from django.core import signing
  2. >>> signer = signing.TimestampSigner()
  3. >>> value = signer.sign_object({"foo": "bar"})
  4. >>> value
  5. 'eyJmb28iOiJiYXIifQ:1kx6R3:D4qGKiptAqo5QW9iv4eNLc6xl4RwiFfes6oOcYhkYnc'
  6. >>> signer.unsign_object(value)
  7. {'foo': 'bar'}
  8. >>> value = signing.dumps({"foo": "bar"})
  9. >>> value
  10. 'eyJmb28iOiJiYXIifQ:1kx6Rf:LBB39RQmME-SRvilheUe5EmPYRbuDBgQp2tCAi7KGLk'
  11. >>> signing.loads(value)
  12. {'foo': 'bar'}

由于 JSON 的性质(没有本地区分列表和元组的方式),如果你传递一个元组,你将从 signing.loads(object) 得到一个列表:

  1. >>> from django.core import signing
  2. >>> value = signing.dumps(("a", "b", "c"))
  3. >>> signing.loads(value)
  4. ['a', 'b', 'c']

dumps(obj, key=None, salt=’django.core.signing’, serializer=JSONSerializer, compress=False)[源代码]

返回 URL 安全的,经过签名的 base64 压缩 JSON 字符串。使用 TimestampSigner 对序列化对象进行签名。

loads(string, key=None, salt=’django.core.signing’, serializer=JSONSerializer, max_age=None, fallback_keys=None)[源代码]

dumps() 相反,如果签名失败引发 BadSignature。如果给定,则检查 max_age (以秒为单位)。