itsdangerous 可以对数据进行加密签名,并将其交给其他不受信任的环境。当拿回数据时,可以确保没有人篡改它。
- 在URL中签署用户ID,并通过电子邮件发送给他们,以取消订阅时事通讯。这样,就不需要生成一次性令牌并将其存储在数据库中。帐户和类似事物的任何激活链接都是一样的。
- 签名对象可以存储在cookie或其他不受信任的源中,这意味着您不需要将会话存储在服务器上,这减少了必要的数据库查询的数量。
- 签名信息可以安全地在服务器和客户端之间进行往返,这使得它们对于将服务器端状态传递给客户端然后返回非常有用。
安装
1
|
pip install -U itsdangerous
|
特性
- 数据签名:确保数据在传输过程中未被篡改。
- 数据序列化:支持将数据序列化为字符串并进行签名。
- 支持多种签名算法:支持HMAC、SHA1、SHA256等多种签名算法。
- 时间戳签名:支持带有时间戳的签名,适用于生成有时效性的令牌。
- 加密和解密:提供基本的加密和解密功能,保护敏感数据。
使用
数据签名验证:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
def test_sign():
# 密钥,应该是独一无二的
SECRET_KEY = 'your-secret-key'
# 创建一个序列化器
serializer = Signer(SECRET_KEY)
# 用于生成签名令牌的数据
data_to_sign = {'user_id': 123}
# 生成签名令牌
token = serializer.sign(str(data_to_sign))
print("Signed Token: ", token)
# 验证签名令牌
try:
# 验证签名并获取数据
data = serializer.unsign(token)
print("Token verified and data loaded: ", data)
except SignatureExpired:
print("The token has expired.")
except BadSignature:
print("The token is invalid or has been tampered with.")
|
执行结果如下:
1
2
3
4
5
6
7
8
9
10
11
|
pytest -s tests/services/signed_token.py::test_sign
================================== test session starts ===================================
platform darwin -- Python 3.12.3, pytest-8.2.2, pluggy-1.5.0
plugins: anyio-4.4.0
collected 1 item
tests/services/signed_token.py Signed Token: b"{'user_id': 123}.BeKLxj-yLc7k1LEJfD0OHFsgiU8"
Token verified and data loaded: b"{'user_id': 123}"
.
=================================== 1 passed in 0.08s ====================================
|
生成的token格式,前部分为明文数据,后面为验证生成的token
数据序列化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
sys.path.append("./")
from itsdangerous import URLSafeSerializer, base64_decode
from itsdangerous import SignatureExpired, BadSignature
def test_url_safe():
# 密钥,应该是独一无二的
SECRET_KEY = 'your-secret-key'
# 创建一个序列化器
serializer = URLSafeSerializer(SECRET_KEY)
# 用于生成签名令牌的数据
data_to_sign = {'user_id': 123}
# 生成签名令牌
token = serializer.dumps(data_to_sign)
print("Signed Token: ", token)
s = "eyJ1c2VyX2lkIjoxMjN9.5zWW1sC3mKAQ13w-l5FzIcCbe9U"
assert token == s
s = base64_decode("eyJ1c2VyX2lkIjoxMjN9")
print("=======", s)
assert data_to_sign == eval(s)
# 验证签名令牌
try:
# 使用loads方法验证签名并获取数据
data = serializer.loads(token)
print("Token verified and data loaded: ", data)
except SignatureExpired:
print("The token has expired.")
except BadSignature:
print("The token is invalid or has been tampered with.")
|
运行测试示例结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
pytest -s tests/services/signed_token.py::test_url_safe
================================== test session starts ===================================
platform darwin -- Python 3.12.3, pytest-8.2.2, pluggy-1.5.0
plugins: anyio-4.4.0
collected 1 item
tests/services/signed_token.py Signed Token: eyJ1c2VyX2lkIjoxMjN9.5zWW1sC3mKAQ13w-l5FzIcCbe9U
======= b'{"user_id":123}'
Token verified and data loaded: {'user_id': 123}
.
=================================== 1 passed in 0.04s ====================================
|
可以发现token的前部分其实就是真实数据base64编码后的结果,所以这个数据验证的token对数据来说是不保密的,只能验证是否被篡改,因此这个数据绝对不要包含敏感数据,否则就会带来安全风险。
带时间截的有效性token:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
def test_url_safe_time():
# 密钥,应该是独一无二的
SECRET_KEY = 'your-secret-key'
# 创建一个序列化器
serializer = URLSafeTimedSerializer(SECRET_KEY)
# 用于生成签名令牌的数据
data_to_sign = {'user_id': 123}
# 生成签名令牌
token = serializer.dumps(data_to_sign)
print("Signed Token: ", token)
s = "eyJ1c2VyX2lkIjoxMjN9.Zryjow.W7kC0CbnIBAKWyiyIbs3qXja8co"
assert token.split(".")[0] == s.split(".")[0]
s = base64_decode("eyJ1c2VyX2lkIjoxMjN9")
print("=======", s)
assert data_to_sign == eval(s)
# 验证签名令牌
try:
# 使用loads方法验证签名并获取数据
data = serializer.loads(token, max_age=30)
print("Token verified and data loaded: ", data)
time.sleep(2)
data = serializer.loads(token, max_age=1)
print("Token verified and data loaded: ", data)
except SignatureExpired:
print("The token has expired.")
except BadSignature:
print("The token is invalid or has been tampered with.")
|
运行结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
pytest -s tests/services/signed_token.py::test_url_safe_time
================================== test session starts ===================================
platform darwin -- Python 3.12.3, pytest-8.2.2, pluggy-1.5.0
plugins: anyio-4.4.0
collected 1 item
tests/services/signed_token.py Signed Token: eyJ1c2VyX2lkIjoxMjN9.ZrylIA.k0iJ9efmZDCE1WZVFhKqCr8q7Hg
======= b'{"user_id":123}'
timestamp b'f\xbc\xa5 '
Token verified and data loaded: {'user_id': 123}
The token has expired.
.
=================================== 1 passed in 2.04s ====================================
|
可以看到token格式只是中间多了个时间截数据,前面部分依然是真实数据。
自定义签名算法
使用SHA256
算法加密签名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import hashlib
class SHA256Signer(Signer):
def get_signature(self, value: bytes | str) -> bytes:
return hashlib.sha256(value + self.secret_key).hexdigest().encode('utf-8')
def verify_signature(self, value: str, sig: str) -> bool:
return self.get_signature(value) == sig
class RequestToken:
""" API请求token """
DEFAULT_EXPIRES_IN = 10
def __init__(self, secret_key, salt=None, expires_in=None):
if expires_in is None:
expires_in = self.DEFAULT_EXPIRES_IN
self.expires_in = expires_in
self.signer = SHA256Signer(secret_key=secret_key, salt=salt)
def generate_token(self, data: str) -> str | bytes:
signed_data = self.signer.sign(data.encode('utf-8'))
return signed_data.decode('utf-8')
def verify_token(self, token: str) -> str | None:
original_data = self.signer.unsign(token)
return original_data.decode('utf-8')
|
测试使用:
1
2
3
4
5
6
7
8
9
10
|
def test_request():
t = RequestToken("key", "salt", 3)
email = "test@example.com"
token = t.generate_token(email)
print("signed token", token)
original_data = t.verify_token(token)
print("original_data", original_data)
assert email == original_data
|
测试结果:
1
2
3
4
5
6
7
8
9
10
11
|
pytest -s tests/services/signed_token.py::test_request
================================== test session starts ===================================
platform darwin -- Python 3.12.3, pytest-8.2.2, pluggy-1.5.0
plugins: anyio-4.4.0
collected 1 item
tests/services/signed_token.py signed token test@example.com.3588dfd5636bdabeffc4147aac8f14d860d1d2c3b65d89fd66a781fe7c1f15d2
original_data test@example.com
.
=================================== 1 passed in 1.00s ====================================
|
参考