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 ====================================

参考