The golden rule of web application security is to never trust data from untrusted sources. Sometimes it can be useful to pass data through an untrusted medium. Cryptographically signed values can be passed through an untrusted channel safe in the knowledge that any tampering will be detected.
Django provides both a low-level API for signing values and a high-level API for setting and reading signed cookies, one of the most common uses of signing in web applications.
你可能还发现签名对以下方面很有用:
- 生成“找回我的账户”URL 以发送给丢失密码的用户。
- 确认存储在表单隐藏字段中的数据未被篡改。
- 生成一次性的秘密 URL,允许临时访问受保护的资源,例如用户付费下载的文件。
Protecting SECRET_KEY
and SECRET_KEY_FALLBACKS
当你使用 startproject
创建一个新的Django项目时,settings.py
文件会自动生成,并随机得到一个 SECRET_KEY
值。这个值是保证签名数据安全的关键——你必须保证这个值的安全,否则攻击者可以用它来生成自己的签名值。
SECRET_KEY_FALLBACKS
can be used to rotate secret keys. The
values will not be used to sign data, but if specified, they will be used to
validate signed data and must be kept secure.
The SECRET_KEY_FALLBACKS
setting was added.
使用低级 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'
If you wish to protect a list, tuple, or dictionary you can do so using the
sign_object()
and unsign_object()
methods:
>>> signed_obj = signer.sign_object({'message': 'Hello!'})
>>> signed_obj
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:Xdc-mOFDjs22KsQAqfVfi8PQSPdo3ckWJxPWwQOFhR4'
>>> obj = signer.unsign_object(signed_obj)
>>> obj
{'message': 'Hello!'}
详见:ref:signing-complex-data
如果签名或值被以任何方式修改,将引发 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, fallback_keys=None)[源代码] Returns a signer which uses
key
to generate signatures andsep
to separate values.sep
cannot be in the URL safe base64 alphabet. This alphabet contains alphanumeric characters, hyphens, and underscores.algorithm
must be an algorithm supported byhashlib
, it defaults to'sha256'
.fallback_keys
is a list of additional values used to validate signed data, defaults toSECRET_KEY_FALLBACKS
.Changed in Django 4.1:The
fallback_keys
argument was added.
使用 salt
参数
如果你不希望一个特定字符串的每一次出现都有相同的签名哈希值,你可以使用 Signer
类的可选 salt
参数。使用盐会将盐和你的 SECRET_KEY
作为签名哈希函数的种子。
>>> signer = Signer()
>>> signer.sign('My string')
'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
>>> signer.sign_object({'message': 'Hello!'})
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:Xdc-mOFDjs22KsQAqfVfi8PQSPdo3ckWJxPWwQOFhR4'
>>> signer = Signer(salt='extra')
>>> signer.sign('My string')
'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
>>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw')
'My string'
>>> signer.sign_object({'message': 'Hello!'})
'eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I'
>>> signer.unsign_object('eyJtZXNzYWdlIjoiSGVsbG8hIn0:-UWSLCE-oUAHzhkHviYz3SOZYBjFKllEOyVZNuUtM-I')
{'message': 'Hello!'}
以这种方式使用盐,会将不同的签名放入不同的命名空间。 来自一个命名空间的签名(一个特定的盐值)不能用于验证在使用不同盐值设置的不同命名空间中的同一明文字符串。这样做的结果是防止攻击者将代码中某个地方生成的签名字符串作为输入,输入到使用不同盐值生成(和验证)签名的另一段代码中。
与你的 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
对象。
-
sign_object
(obj, serializer=JSONSerializer, compress=False) Encode, optionally compress, append current timestamp, and sign complex data structure (e.g. list, tuple, or dictionary).
-
unsign_object
(signed_obj, serializer=JSONSerializer, max_age=None) Checks if
signed_obj
was signed less thanmax_age
seconds ago, otherwise raisesSignatureExpired
. Themax_age
parameter can accept an integer or adatetime.timedelta
object.
-
保护复杂的数据结构
If you wish to protect a list, tuple or dictionary you can do so using the
Signer.sign_object()
and unsign_object()
methods, or signing module's
dumps()
or loads()
functions (which are shortcuts for
TimestampSigner(salt='django.core.signing').sign_object()/unsign_object()
).
These use JSON serialization under the hood. JSON ensures that even if your
SECRET_KEY
is stolen an attacker will not be able to execute
arbitrary commands by exploiting the pickle format:
>>> from django.core import signing
>>> signer = signing.TimestampSigner()
>>> value = signer.sign_object({'foo': 'bar'})
>>> value
'eyJmb28iOiJiYXIifQ:1kx6R3:D4qGKiptAqo5QW9iv4eNLc6xl4RwiFfes6oOcYhkYnc'
>>> signer.unsign_object(value)
{'foo': 'bar'}
>>> value = signing.dumps({'foo': 'bar'})
>>> value
'eyJmb28iOiJiYXIifQ:1kx6Rf:LBB39RQmME-SRvilheUe5EmPYRbuDBgQp2tCAi7KGLk'
>>> 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, fallback_keys=None)[源代码] 与
dumps()
相反,如果签名失败引发BadSignature
。如果给定,则检查max_age
(以秒为单位)。Changed in Django 4.1:The
fallback_keys
argument was added.
讨论区