加密签名¶
Web 应用程序安全的黄金法则是永远不要信任来自不受信任来源的数据。有时,通过不受信任的媒介传递数据是有用的。经过加密签名的值可以通过不受信任的渠道传递,因为知道任何篡改都会被检测到。
Django 提供了用于签署值的低级 API 和用于设置和读取已签名 cookie 的高级 API,这是 Web 应用程序中签名的最常见用法之一。
你可能还发现签名对以下方面很有用:
- 生成“找回我的账户”URL 以发送给丢失密码的用户。
- 确认存储在表单隐藏字段中的数据未被篡改。
- 生成一次性的秘密 URL,允许临时访问受保护的资源,例如用户付费下载的文件。
保护 SECRET_KEY
¶
当你使用 startproject
创建一个新的Django项目时,settings.py
文件会自动生成,并随机得到一个 SECRET_KEY
值。这个值是保证签名数据安全的关键——你必须保证这个值的安全,否则攻击者可以用它来生成自己的签名值。
使用低级 API¶
Django 的签名方法位于 django.core.signing
模块中。要签署一个值,首先要实例化一个 Signer
实例:
>>> from django.core.signing import Signer
>>> signer = Signer()
>>> value = signer.sign('My string')
>>> value
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
签名被附加在字符串的结尾,在冒号之后。你可以使用 unsign
方法检索原始值:
>>> original = signer.unsign(value)
>>> original
'My string'
如果你将非字符串值传递给 sign
,该值将在被签署前被强制变成字符串,并且 unsign
结果将返回此字符串值:
>>> signed = signer.sign(2.5)
>>> original = signer.unsign(signed)
>>> original
'2.5'
如果签名或值被以任何方式修改,将引发 django.core.signing.BadSignature
异常:
>>> from django.core import signing
>>> value += 'm'
>>> try:
... original = signer.unsign(value)
... except signing.BadSignature:
... print("Tampering detected!")
默认情况下,Signer
类使用 SECRET_KEY
配置来生成签名。你可以使用不同的密钥传入 Signer
构造函数生成不同的签名:
>>> signer = Signer('my-other-secret')
>>> value = signer.sign('My string')
>>> value
'My string:EkfQJafvGyiofrdGnuthdxImIJw'
-
class
Signer
(key=None, sep=':', salt=None, algorithm=None)¶ 返回一个使用
key
生成签名并使用sep
分隔值的签名器。sep
不能在 URL 安全 base64 字母表 中。这个字母表包含字母数字字符、连字符和下划线。algorithm
必须是hashlib
支持的算法。默认为'sha256'
。Changed in Django 3.1:加入
algorithm
参数。
使用 salt
参数¶
如果你不希望一个特定字符串的每一次出现都有相同的签名哈希值,你可以使用 Signer
类的可选 salt
参数。使用盐会将盐和你的 SECRET_KEY
作为签名哈希函数的种子。
>>> signer = Signer()
>>> signer.sign('My string')
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
>>> signer = Signer(salt='extra')
>>> signer.sign('My string')
'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
>>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw')
'My string'
以这种方式使用盐,会将不同的签名放入不同的命名空间。 来自一个命名空间的签名(一个特定的盐值)不能用于验证在使用不同盐值设置的不同命名空间中的同一明文字符串。这样做的结果是防止攻击者将代码中某个地方生成的签名字符串作为输入,输入到使用不同盐值生成(和验证)签名的另一段代码中。
与你的 SECRET_KEY
不同,你的盐参数不需要保密。
验证时间戳值¶
TimestampSigner
是 Signer
的子类,它给值附加一个签名的时间戳。这允许你确认一个签名的值是在特定时间内创建的:
>>> from datetime import timedelta
>>> from django.core.signing import TimestampSigner
>>> signer = TimestampSigner()
>>> value = signer.sign('hello')
>>> value
'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
>>> signer.unsign(value)
'hello'
>>> signer.unsign(value, max_age=10)
...
SignatureExpired: Signature age 15.5289158821 > 10 seconds
>>> signer.unsign(value, max_age=20)
'hello'
>>> signer.unsign(value, max_age=timedelta(seconds=20))
'hello'
-
class
TimestampSigner
(key=None, sep=':', salt=None, algorithm='sha256')¶ -
sign
(value)¶ 签名
value
并附加当前时间戳。
-
unsign
(value, max_age=None)¶ 检查
value
是否在max_age
秒前被签署,否则引发SignatureExpired
。max_age
参数可以接受一个整数或一个datetime.timedelta
对象。
Changed in Django 3.1:加入
algorithm
参数。-
保护复杂的数据结构¶
如果要保护列表、元组或字典,可以使用签名模块的 dumps
和 loads
函数。这些函数模仿了 Python 的 pickle 模块,但在幕后使用 JSON 序列化。JSON 确保即使你的 SECRET_KEY
被窃取,攻击者也无法通过利用 pickle 格式执行任意命令:
>>> from django.core import signing
>>> value = signing.dumps({"foo": "bar"})
>>> value
'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
>>> signing.loads(value)
{'foo': 'bar'}
由于 JSON 的特性(列表和元组之间没有原生的区别),如果你传入一个元组,你将从 signing.loads(object)
得到一个列表:
>>> from django.core import signing
>>> value = signing.dumps(('a','b','c'))
>>> signing.loads(value)
['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)¶ 与
dumps()
相反,如果签名失败引发BadSignature
。如果给定,则检查max_age
(以秒为单位)。