加密签名¶
Web 应用安全的黄金法则是永远不要信任来自不可信来源的数据。有时,通过不可信渠道传递数据可能会很有用。通过加密签名的值可以通过不可信渠道传递,因为我们可以安全地知道任何篡改都将被检测到。
Django 提供了用于签名值的低级 API 和用于设置和读取签名 cookie 的高级 API,签名在 web 应用程序中最常见的用途之一就是签名 cookie。
你可能还发现签名对以下方面很有用:
生成“找回我的账户”URL 以发送给丢失密码的用户。
确认存储在表单隐藏字段中的数据未被篡改。
生成一次性的秘密 URL,允许临时访问受保护的资源,例如用户付费下载的文件。
保护 SECRET_KEY 和 SECRET_KEY_FALLBACKS¶
当你使用 startproject 创建一个新的Django项目时,settings.py 文件会自动生成,并随机得到一个 SECRET_KEY 值。这个值是保证签名数据安全的关键——你必须保证这个值的安全,否则攻击者可以用它来生成自己的签名值。
SECRET_KEY_FALLBACKS 可以用于轮换密钥。这些值不会用于签署数据,但如果指定了,它们将用于验证签署的数据,并必须保持安全。
使用低级 API¶
Django 的签名方法位于 django.core.signing 模块中。要签署一个值,首先实例化一个 Signer 实例:
>>> from django.core.signing import Signer
>>> signer = Signer()
>>> value = signer.sign("My string")
>>> value
'My string:v9G-nxfz3iQGTXrePqYPlGvH79WTcIgj1QIQSUODTW0'
签名被追加到字符串的末尾,跟在冒号后面。你可以使用 unsign 方法检索原始值:
>>> original = signer.unsign(value)
>>> original
'My string'
如果你将一个非字符串值传递给 sign,该值将在签名之前被强制转换为字符串,而 unsign 的结果将给你该字符串值:
>>> signed = signer.sign(2.5)
>>> original = signer.unsign(signed)
>>> original
'2.5'
如果你希望保护一个列表、元组或字典,可以使用 sign_object() 和 unsign_object() 方法来实现:
>>> signed_obj = signer.sign_object({"message": "Hello!"})
>>> signed_obj
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:bzb48DBkB-bwLaCnUVB75r5VAPUEpzWJPrTb80JMIXM'
>>> obj = signer.unsign_object(signed_obj)
>>> obj
{'message': 'Hello!'}
详见 保护复杂的数据结构。
如果签名或值已经被以任何方式更改,将会引发 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(key="my-other-secret")
>>> value = signer.sign("My string")
>>> value
'My string:o3DrrsT6JRB73t-HDymfDNbTSxfMlom2d8TiUlb1hWY'
- class Signer(*, key=None, sep=':', salt=None, algorithm=None, fallback_keys=None)[source]¶
Returns a signer which uses
keyto generate signatures andsepto separate values.sepcannot be in the URL safe base64 alphabet. This alphabet contains alphanumeric characters, hyphens, and underscores.algorithmmust be an algorithm supported byhashlib, it defaults to'sha256'.fallback_keysis a list of additional values used to validate signed data, defaults toSECRET_KEY_FALLBACKS.
使用 salt 参数¶
如果你不希望每次出现特定字符串时都具有相同的签名哈希值,可以使用 Signer 类的可选参数 salt。使用盐会为签名哈希函数提供盐和你的 SECRET_KEY:
>>> signer = Signer()
>>> signer.sign("My string")
'My string:v9G-nxfz3iQGTXrePqYPlGvH79WTcIgj1QIQSUODTW0'
>>> signer.sign_object({"message": "Hello!"})
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:bzb48DBkB-bwLaCnUVB75r5VAPUEpzWJPrTb80JMIXM'
>>> signer = Signer(salt="extra")
>>> signer.sign("My string")
'My string:YMD-FR6rof3heDkFRffdmG4pXbAZSOtb-aQxg3vmmfc'
>>> signer.unsign("My string:YMD-FR6rof3heDkFRffdmG4pXbAZSOtb-aQxg3vmmfc")
'My string'
>>> signer.sign_object({"message": "Hello!"})
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I'
>>> signer.unsign_object(
... "eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I"
... )
{'message': 'Hello!'}
Using salt in this way puts the different signatures into different namespaces. A signature that comes from one namespace (a particular salt value) cannot be used to validate the same plaintext string in a different namespace that is using a different salt setting. The result is to prevent an attacker from using a signed string generated in one place in the code as input to another piece of code that is generating (and verifying) signatures using a different salt.
与你的 SECRET_KEY 不同,你的盐参数不需要保密。
验证时间戳值¶
TimestampSigner 是 Signer 的子类,它会在值后附加一个带签名的时间戳。这允许你确认一个签名值是否在指定的时间段内创建:
>>> from datetime import timedelta
>>> from django.core.signing import TimestampSigner
>>> signer = TimestampSigner()
>>> value = signer.sign("hello")
>>> value
'hello:1stLqR:_rvr4oXCgT4HyfwjXaU39QvTnuNuUthFRCzNOy4Hqt0'
>>> 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')[source]¶
-
- unsign(value, max_age=None)[source]¶
Checks if
valuewas signed less thanmax_ageseconds ago, otherwise raisesSignatureExpired. Themax_ageparameter can accept an integer or adatetime.timedeltaobject.
- sign_object(obj, serializer=JSONSerializer, compress=False)¶
对复杂的数据结构(例如列表、元组或字典)进行编码,可选地压缩数据,追加当前时间戳,并对其进行签名。
- unsign_object(signed_obj, serializer=JSONSerializer, max_age=None)¶
Checks if
signed_objwas signed less thanmax_ageseconds ago, otherwise raisesSignatureExpired. Themax_ageparameter can accept an integer or adatetime.timedeltaobject.
保护复杂的数据结构¶
如果你希望保护一个列表、元组或字典,可以使用 Signer.sign_object() 和 unsign_object() 方法,或者使用签名模块的 dumps() 或 loads() 函数(它们是 TimestampSigner(salt='django.core.signing').sign_object()/unsign_object() 的快捷方式)。这些方法在底层使用了 JSON 序列化。JSON 确保即使你的 SECRET_KEY 被窃取,攻击者也无法通过利用 pickle 格式来执行任意命令:
>>> from django.core import signing
>>> signer = signing.TimestampSigner()
>>> value = signer.sign_object({"foo": "bar"})
>>> value
'eyJmb28iOiJiYXIifQ:1stLrZ:_QiOBHafwucBF9FyAr54qEs84ZO1UdsO1XiTJCvvdno'
>>> signer.unsign_object(value)
{'foo': 'bar'}
>>> value = signing.dumps({"foo": "bar"})
>>> value
'eyJmb28iOiJiYXIifQ:1stLsC:JItq2ZVjmAK6ivrWI-v1Gk1QVf2hOF52oaEqhZHca7I'
>>> 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)[source]¶
返回 URL 安全的,经过签名的 base64 压缩 JSON 字符串。使用
TimestampSigner对序列化对象进行签名。