加密签名

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,允许临时访问受保护的资源,例如用户付费下载的文件。

保护 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'

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!'}

See 保护复杂的数据结构 for more details.

如果签名或值被以任何方式修改,将引发 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.2:

The sign_object() and unsign_object() methods were 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 不同,你的盐参数不需要保密。

Changed in Django 3.2:

The sign_object() and unsign_object() methods were added.

验证时间戳值

TimestampSignerSigner 的子类,它给值附加一个签名的时间戳。这允许你确认一个签名的值是在特定时间内创建的:

>>> 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 秒前被签署,否则引发 SignatureExpiredmax_age 参数可以接受一个整数或一个 datetime.timedelta 对象。

sign_object(obj, serializer=JSONSerializer, compress=False)
New in Django 3.2.

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)
New in Django 3.2.

Checks if signed_obj was signed less than max_age seconds ago, otherwise raises SignatureExpired. The max_age parameter can accept an integer or a datetime.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)

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

Changed in Django 3.2:

The sign_object() and unsign_object() methods were added.