25.pytest的接口签名封装

Pytest 接口签名封装详解 – 新手完整教程

1. 什么是接口签名

1.1 接口签名的基本概念

接口签名(API Signature)是一种用于验证接口请求合法性和完整性的安全机制。它通过对请求参数进行加密计算,生成一个唯一的签名字符串,服务器通过验证这个签名来判断请求是否合法、是否被篡改。

1.2 为什么需要接口签名

想象一下,如果你在电商网站购买商品:

没有签名的情况:

客户端发送:我要购买商品A,价格100元
服务器收到:我要购买商品A,价格100元

有签名的情况:

客户端发送:
  - 商品ID: A
  - 价格: 100元
  - 签名: a8f5f167f44f4964e6c998dee827110c(通过参数+密钥计算得出)

服务器收到后:
  1. 使用相同规则计算签名
  2. 对比签名是否一致
  3. 一致则处理请求,不一致则拒绝

接口签名的主要作用:

  1. 防止数据篡改:如果有人修改了请求参数(比如把价格从100改成1),签名就会不匹配,请求会被拒绝
  2. 身份验证:只有拥有正确密钥的客户端才能生成正确的签名
  3. 防止重放攻击:通过时间戳和随机数,确保每个请求都是唯一的
  4. 数据完整性:确保数据在传输过程中没有被修改

1.3 接口签名的应用场景

1.3.1 支付接口签名

# 示例:支付接口需要签名验证
# 原始请求参数
{
    "order_id": "20240101001",
    "amount": "100.00",
    "user_id": "1001"
}

# 生成签名后
{
    "order_id": "20240101001",
    "amount": "100.00",
    "user_id": "1001",
    "timestamp": "1704067200",
    "nonce": "abc123def456",
    "sign": "A8F5F167F44F4964E6C998DEE827110C"  # 签名
}

1.3.2 敏感数据接口签名

# 示例:用户信息修改接口
# 原始请求
{
    "user_id": "1001",
    "phone": "13800138000",
    "email": "user@example.com"
}

# 带签名的请求
{
    "user_id": "1001",
    "phone": "13800138000",
    "email": "user@example.com",
    "sign": "B9G6G278G55G5075F7D009EFF938221D"  # 签名
}

1.3.3 API 调用签名

# 示例:第三方API调用
# 原始请求
{
    "api_key": "your_api_key",
    "action": "get_data",
    "params": {"id": 123}
}

# 带签名的请求
{
    "api_key": "your_api_key",
    "action": "get_data",
    "params": {"id": 123},
    "timestamp": "1704067200",
    "sign": "C0H7H389H66H6086G8E110FGG049332E"  # 签名
}

1.4 签名与加密的区别

很多新手容易混淆签名和加密,这里详细说明一下:

加密(Encryption):

  • 目的:隐藏数据内容,防止数据被读取
  • 特点:可以解密,还原原始数据
  • 示例:AES加密、RSA加密
  • 用途:传输敏感数据(如密码、身份证号)

签名(Signature):

  • 目的:验证数据完整性和来源,不隐藏数据内容
  • 特点:不可逆,不能还原原始数据
  • 示例:MD5签名、SHA256签名
  • 用途:验证请求合法性、防止数据篡改

简单理解:

  • 加密 = 把信装在保险箱里(隐藏内容)
  • 签名 = 在信封上盖章(验证来源和完整性)

2. 签名算法的基本原理

2.1 签名的生成流程

签名的生成通常遵循以下步骤:

步骤1:准备参数
  ↓
步骤2:参数排序(按键名排序)
  ↓
步骤3:拼接字符串(key1=value1&key2=value2)
  ↓
步骤4:添加密钥(sign_string + secret_key)
  ↓
步骤5:计算签名(MD5/SHA256等)
  ↓
步骤6:格式化签名(转大写/小写)
  ↓
步骤7:添加到请求参数

2.2 签名的验证流程

服务器验证签名的流程:

步骤1:接收请求参数
  ↓
步骤2:提取签名(从参数中取出sign字段)
  ↓
步骤3:移除签名字段
  ↓
步骤4:使用相同规则重新计算签名
  ↓
步骤5:对比两个签名是否一致
  ↓
步骤6:一致则通过,不一致则拒绝

2.3 签名的关键要素

1. 参数排序

  • 必须按照统一的规则排序(通常是按键名ASCII码排序)
  • 确保客户端和服务器使用相同的排序规则

2. 字符串拼接

  • 格式通常是:key1=value1&key2=value2
  • 注意空值的处理方式(是否参与签名)

3. 密钥(Secret Key)

  • 客户端和服务器共享的密钥
  • 绝对不能泄露,否则签名机制失效

4. 签名算法

  • 常用的有:MD5、SHA1、SHA256、HMAC-SHA256
  • 必须使用相同的算法

5. 时间戳和随机数

  • 防止重放攻击
  • 确保每个请求的唯一性

3. 常见的签名算法详解

3.1 MD5 签名算法

3.1.1 MD5 签名的特点

MD5(Message Digest Algorithm 5) 是一种广泛使用的签名算法:

  • 优点:计算速度快,实现简单
  • 缺点:安全性相对较低,已被证明存在碰撞漏洞
  • 适用场景:对安全性要求不高的内部系统

3.1.2 MD5 签名的基本实现

import hashlib

def md5_sign_basic(params, secret_key):
    """
    基础 MD5 签名生成

    参数:
        params: 请求参数字典
        secret_key: 密钥

    返回:
        签名字符串
    """
    # 步骤1:移除签名字段(如果存在)
    sign_params = {k: v for k, v in params.items() if k != 'sign'}

    # 步骤2:按键名排序
    sorted_params = sorted(sign_params.items())

    # 步骤3:拼接成字符串
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])

    # 步骤4:在末尾拼接密钥
    sign_string += f"&key={secret_key}"

    # 步骤5:计算 MD5
    sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest()

    # 步骤6:转换为大写(根据实际需求)
    return sign.upper()

# 使用示例
params = {
    "order_id": "20240101001",
    "amount": "100.00",
    "timestamp": "1704067200"
}
secret_key = "my_secret_key_123"

sign = md5_sign_basic(params, secret_key)
print(f"签名: {sign}")
# 输出:签名: A8F5F167F44F4964E6C998DEE827110C

# 将签名添加到参数中
params["sign"] = sign
print(f"最终参数: {params}")

3.1.3 MD5 签名的详细示例

让我们通过一个完整的示例来理解 MD5 签名的每一步:

import hashlib

def md5_sign_detailed(params, secret_key):
    """
    详细的 MD5 签名生成(带步骤说明)
    """
    print("=" * 60)
    print("MD5 签名生成过程")
    print("=" * 60)

    # 步骤1:原始参数
    print(f"n步骤1:原始参数")
    print(f"  {params}")

    # 步骤2:移除签名字段
    sign_params = {k: v for k, v in params.items() if k != 'sign'}
    print(f"n步骤2:移除签名字段后")
    print(f"  {sign_params}")

    # 步骤3:按键名排序
    sorted_params = sorted(sign_params.items())
    print(f"n步骤3:按键名排序后")
    for k, v in sorted_params:
        print(f"  {k} = {v}")

    # 步骤4:拼接成字符串
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])
    print(f"n步骤4:拼接成字符串")
    print(f"  {sign_string}")

    # 步骤5:添加密钥
    sign_string_with_key = sign_string + f"&key={secret_key}"
    print(f"n步骤5:添加密钥后")
    print(f"  {sign_string_with_key}")

    # 步骤6:计算 MD5
    sign_bytes = sign_string_with_key.encode('utf-8')
    md5_hash = hashlib.md5(sign_bytes)
    sign_hex = md5_hash.hexdigest()
    print(f"n步骤6:计算 MD5")
    print(f"  原始字节: {sign_bytes}")
    print(f"  MD5值(小写): {sign_hex}")

    # 步骤7:转换为大写
    sign_upper = sign_hex.upper()
    print(f"n步骤7:转换为大写")
    print(f"  最终签名: {sign_upper}")
    print("=" * 60)

    return sign_upper

# 使用示例
params = {
    "order_id": "20240101001",
    "amount": "100.00",
    "timestamp": "1704067200"
}
secret_key = "my_secret_key_123"

sign = md5_sign_detailed(params, secret_key)

运行结果:

============================================================
MD5 签名生成过程
============================================================

步骤1:原始参数
  {'order_id': '20240101001', 'amount': '100.00', 'timestamp': '1704067200'}

步骤2:移除签名字段后
  {'order_id': '20240101001', 'amount': '100.00', 'timestamp': '1704067200'}

步骤3:按键名排序后
  amount = 100.00
  order_id = 20240101001
  timestamp = 1704067200

步骤4:拼接成字符串
  amount=100.00&order_id=20240101001&timestamp=1704067200

步骤5:添加密钥后
  amount=100.00&order_id=20240101001&timestamp=1704067200&key=my_secret_key_123

步骤6:计算 MD5
  原始字节: b'amount=100.00&order_id=20240101001&timestamp=1704067200&key=my_secret_key_123'
  MD5值(小写): a8f5f167f44f4964e6c998dee827110c

步骤7:转换为大写
  最终签名: A8F5F167F44F4964E6C998DEE827110C
============================================================

3.1.4 MD5 签名的变体

不同的系统可能有不同的签名规则,这里介绍几种常见的变体:

变体1:密钥放在前面

def md5_sign_key_first(params, secret_key):
    """密钥放在前面的签名方式"""
    sign_params = {k: v for k, v in params.items() if k != 'sign'}
    sorted_params = sorted(sign_params.items())
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])

    # 密钥放在前面
    sign_string = f"key={secret_key}&{sign_string}"

    sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
    return sign

变体2:不拼接密钥,直接计算

def md5_sign_no_key_append(params, secret_key):
    """不拼接密钥,直接计算(较少见)"""
    sign_params = {k: v for k, v in params.items() if k != 'sign'}
    sorted_params = sorted(sign_params.items())
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])

    # 使用 HMAC-MD5(需要密钥)
    import hmac
    sign = hmac.new(
        secret_key.encode('utf-8'),
        sign_string.encode('utf-8'),
        hashlib.md5
    ).hexdigest().upper()
    return sign

变体3:排除空值

def md5_sign_exclude_empty(params, secret_key):
    """排除空值的签名方式"""
    # 移除签名字段和空值
    sign_params = {k: v for k, v in params.items() 
                   if k != 'sign' and v is not None and v != ''}

    sorted_params = sorted(sign_params.items())
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])
    sign_string += f"&key={secret_key}"

    sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
    return sign

3.2 SHA256 签名算法

3.2.1 SHA256 签名的特点

SHA256(Secure Hash Algorithm 256) 是一种更安全的签名算法:

  • 优点:安全性高,抗碰撞能力强
  • 缺点:计算速度稍慢于 MD5
  • 适用场景:对安全性要求较高的系统

3.2.2 SHA256 签名的基本实现

import hashlib
import hmac

def sha256_sign_basic(params, secret_key):
    """
    基础 SHA256 签名生成(使用 HMAC)

    参数:
        params: 请求参数字典
        secret_key: 密钥

    返回:
        签名字符串
    """
    # 步骤1:移除签名字段
    sign_params = {k: v for k, v in params.items() if k != 'sign'}

    # 步骤2:按键名排序
    sorted_params = sorted(sign_params.items())

    # 步骤3:拼接成字符串
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])

    # 步骤4:使用 HMAC-SHA256 计算签名
    signature = hmac.new(
        secret_key.encode('utf-8'),  # 密钥
        sign_string.encode('utf-8'),  # 消息
        hashlib.sha256  # 算法
    ).hexdigest()

    # 步骤5:转换为大写
    return signature.upper()

# 使用示例
params = {
    "order_id": "20240101001",
    "amount": "100.00",
    "timestamp": "1704067200"
}
secret_key = "my_secret_key_123"

sign = sha256_sign_basic(params, secret_key)
print(f"SHA256 签名: {sign}")

3.2.3 SHA256 签名的详细示例

import hashlib
import hmac

def sha256_sign_detailed(params, secret_key):
    """
    详细的 SHA256 签名生成(带步骤说明)
    """
    print("=" * 60)
    print("SHA256 签名生成过程")
    print("=" * 60)

    # 步骤1:原始参数
    print(f"n步骤1:原始参数")
    print(f"  {params}")

    # 步骤2:移除签名字段
    sign_params = {k: v for k, v in params.items() if k != 'sign'}
    print(f"n步骤2:移除签名字段后")
    print(f"  {sign_params}")

    # 步骤3:按键名排序
    sorted_params = sorted(sign_params.items())
    print(f"n步骤3:按键名排序后")
    for k, v in sorted_params:
        print(f"  {k} = {v}")

    # 步骤4:拼接成字符串
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])
    print(f"n步骤4:拼接成字符串")
    print(f"  {sign_string}")

    # 步骤5:使用 HMAC-SHA256 计算签名
    signature = hmac.new(
        secret_key.encode('utf-8'),
        sign_string.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    print(f"n步骤5:使用 HMAC-SHA256 计算签名")
    print(f"  密钥: {secret_key}")
    print(f"  消息: {sign_string}")
    print(f"  SHA256值(小写): {signature}")

    # 步骤6:转换为大写
    sign_upper = signature.upper()
    print(f"n步骤6:转换为大写")
    print(f"  最终签名: {sign_upper}")
    print("=" * 60)

    return sign_upper

# 使用示例
params = {
    "order_id": "20240101001",
    "amount": "100.00",
    "timestamp": "1704067200"
}
secret_key = "my_secret_key_123"

sign = sha256_sign_detailed(params, secret_key)

3.2.4 SHA256 签名的变体

变体1:不使用 HMAC,直接 SHA256

def sha256_sign_direct(params, secret_key):
    """直接使用 SHA256(不推荐,安全性较低)"""
    sign_params = {k: v for k, v in params.items() if k != 'sign'}
    sorted_params = sorted(sign_params.items())
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])

    # 拼接密钥
    sign_string += f"&key={secret_key}"

    # 直接计算 SHA256
    sign = hashlib.sha256(sign_string.encode('utf-8')).hexdigest().upper()
    return sign

变体2:SHA256 后再次 SHA256(双重哈希)

def sha256_sign_double(params, secret_key):
    """双重 SHA256 签名"""
    sign_params = {k: v for k, v in params.items() if k != 'sign'}
    sorted_params = sorted(sign_params.items())
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])
    sign_string += f"&key={secret_key}"

    # 第一次 SHA256
    first_hash = hashlib.sha256(sign_string.encode('utf-8')).hexdigest()

    # 第二次 SHA256
    second_hash = hashlib.sha256(first_hash.encode('utf-8')).hexdigest().upper()

    return second_hash

3.3 带时间戳和随机数的签名

3.3.1 为什么需要时间戳和随机数

时间戳(Timestamp)的作用:

  • 防止重放攻击:服务器可以拒绝过期的请求
  • 记录请求时间:便于日志分析和问题排查

随机数(Nonce)的作用:

  • 确保请求唯一性:即使参数相同,签名也不同
  • 防止重放攻击:服务器可以记录已使用的 nonce

3.3.2 带时间戳和随机数的签名实现

import hashlib
import time
import random
import string

def sign_with_timestamp_nonce(params, secret_key, sign_type='md5'):
    """
    生成带时间戳和随机数的签名

    参数:
        params: 请求参数字典
        secret_key: 密钥
        sign_type: 签名类型('md5' 或 'sha256')

    返回:
        (签名字符串, 更新后的参数字典)
    """
    # 步骤1:添加时间戳
    params["timestamp"] = str(int(time.time()))
    print(f"添加时间戳: {params['timestamp']}")

    # 步骤2:添加随机数
    nonce = ''.join(random.choices(
        string.ascii_letters + string.digits, k=16
    ))
    params["nonce"] = nonce
    print(f"添加随机数: {nonce}")

    # 步骤3:移除签名字段
    sign_params = {k: v for k, v in params.items() if k != 'sign'}

    # 步骤4:按键名排序
    sorted_params = sorted(sign_params.items())

    # 步骤5:拼接成字符串
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])

    # 步骤6:计算签名
    if sign_type.lower() == 'sha256':
        import hmac
        sign = hmac.new(
            secret_key.encode('utf-8'),
            sign_string.encode('utf-8'),
            hashlib.sha256
        ).hexdigest().upper()
    else:
        sign_string += f"&key={secret_key}"
        sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()

    # 步骤7:将签名添加到参数中
    params["sign"] = sign

    return sign, params

# 使用示例
params = {
    "order_id": "20240101001",
    "amount": "100.00"
}
secret_key = "my_secret_key_123"

sign, final_params = sign_with_timestamp_nonce(params, secret_key, 'md5')
print(f"n签名: {sign}")
print(f"最终参数: {final_params}")

3.3.3 时间戳格式的处理

不同的系统可能要求不同的时间戳格式:

import time
from datetime import datetime

def get_timestamp(format_type="unix"):
    """
    获取时间戳(不同格式)

    参数:
        format_type: 格式类型
            - "unix": Unix 时间戳(秒)
            - "unix_ms": Unix 时间戳(毫秒)
            - "datetime": 日期时间字符串
            - "date": 日期字符串
    """
    if format_type == "unix":
        return str(int(time.time()))
    elif format_type == "unix_ms":
        return str(int(time.time() * 1000))
    elif format_type == "datetime":
        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    elif format_type == "date":
        return datetime.now().strftime("%Y-%m-%d")
    else:
        return str(int(time.time()))

# 使用示例
print(f"Unix 时间戳(秒): {get_timestamp('unix')}")
print(f"Unix 时间戳(毫秒): {get_timestamp('unix_ms')}")
print(f"日期时间字符串: {get_timestamp('datetime')}")
print(f"日期字符串: {get_timestamp('date')}")

4. 在 Pytest 中封装签名工具类

4.1 创建签名工具类

4.1.1 基础签名工具类

首先,我们创建一个基础的签名工具类:

# utils/sign_utils.py
"""
签名工具类
提供各种签名算法的封装
"""
import hashlib
import hmac
import time
import random
import string
from typing import Dict, Any, Optional, Tuple

class SignUtils:
    """签名工具类"""

    @staticmethod
    def generate_md5_sign(
        params: Dict[str, Any],
        secret_key: str,
        exclude_keys: Optional[list] = None,
        upper: bool = True,
        key_position: str = "end"
    ) -> str:
        """
        生成 MD5 签名

        参数:
            params: 请求参数字典
            secret_key: 密钥
            exclude_keys: 排除的键名列表(如 ['sign', 'signature'])
            upper: 是否转换为大写
            key_position: 密钥位置('end' 或 'begin')

        返回:
            签名字符串

        示例:
            params = {"order_id": "123", "amount": "100"}
            secret_key = "my_key"
            sign = SignUtils.generate_md5_sign(params, secret_key)
        """
        # 1. 复制参数,避免修改原字典
        sign_params = params.copy()

        # 2. 移除签名字段和排除的字段
        exclude_keys = exclude_keys or []
        exclude_keys.extend(['sign', 'signature'])
        sign_params = {k: v for k, v in sign_params.items() 
                      if k not in exclude_keys}

        # 3. 移除空值(可选,根据实际需求)
        sign_params = {k: v for k, v in sign_params.items() 
                      if v is not None and v != ''}

        # 4. 按键名排序
        sorted_params = sorted(sign_params.items())

        # 5. 拼接成字符串
        sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])

        # 6. 添加密钥
        if key_position == "begin":
            sign_string = f"key={secret_key}&{sign_string}"
        else:
            sign_string += f"&key={secret_key}"

        # 7. 计算 MD5
        sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest()

        # 8. 转换为大写(如果需要)
        return sign.upper() if upper else sign

    @staticmethod
    def generate_sha256_sign(
        params: Dict[str, Any],
        secret_key: str,
        exclude_keys: Optional[list] = None,
        upper: bool = True,
        use_hmac: bool = True
    ) -> str:
        """
        生成 SHA256 签名

        参数:
            params: 请求参数字典
            secret_key: 密钥
            exclude_keys: 排除的键名列表
            upper: 是否转换为大写
            use_hmac: 是否使用 HMAC(推荐)

        返回:
            签名字符串
        """
        # 1. 复制参数
        sign_params = params.copy()

        # 2. 移除签名字段和排除的字段
        exclude_keys = exclude_keys or []
        exclude_keys.extend(['sign', 'signature'])
        sign_params = {k: v for k, v in sign_params.items() 
                      if k not in exclude_keys}

        # 3. 移除空值
        sign_params = {k: v for k, v in sign_params.items() 
                      if v is not None and v != ''}

        # 4. 按键名排序
        sorted_params = sorted(sign_params.items())

        # 5. 拼接成字符串
        sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])

        # 6. 计算签名
        if use_hmac:
            # 使用 HMAC-SHA256(推荐)
            signature = hmac.new(
                secret_key.encode('utf-8'),
                sign_string.encode('utf-8'),
                hashlib.sha256
            ).hexdigest()
        else:
            # 直接 SHA256(不推荐)
            sign_string += f"&key={secret_key}"
            signature = hashlib.sha256(sign_string.encode('utf-8')).hexdigest()

        # 7. 转换为大写(如果需要)
        return signature.upper() if upper else signature

    @staticmethod
    def generate_sign_with_timestamp(
        params: Dict[str, Any],
        secret_key: str,
        sign_type: str = 'md5',
        add_timestamp: bool = True,
        add_nonce: bool = True,
        timestamp_format: str = "unix",
        nonce_length: int = 16
    ) -> Tuple[str, Dict[str, Any]]:
        """
        生成带时间戳和随机数的签名

        参数:
            params: 请求参数字典
            secret_key: 密钥
            sign_type: 签名类型('md5' 或 'sha256')
            add_timestamp: 是否添加时间戳
            add_nonce: 是否添加随机数
            timestamp_format: 时间戳格式('unix', 'unix_ms', 'datetime')
            nonce_length: 随机数长度

        返回:
            (签名字符串, 更新后的参数字典)
        """
        # 1. 复制参数
        final_params = params.copy()

        # 2. 添加时间戳
        if add_timestamp:
            if timestamp_format == "unix":
                final_params["timestamp"] = str(int(time.time()))
            elif timestamp_format == "unix_ms":
                final_params["timestamp"] = str(int(time.time() * 1000))
            elif timestamp_format == "datetime":
                from datetime import datetime
                final_params["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            else:
                final_params["timestamp"] = str(int(time.time()))

        # 3. 添加随机数
        if add_nonce:
            nonce = ''.join(random.choices(
                string.ascii_letters + string.digits, k=nonce_length
            ))
            final_params["nonce"] = nonce

        # 4. 生成签名
        if sign_type.lower() == 'sha256':
            sign = SignUtils.generate_sha256_sign(final_params, secret_key)
        else:
            sign = SignUtils.generate_md5_sign(final_params, secret_key)

        # 5. 将签名添加到参数中
        final_params["sign"] = sign

        return sign, final_params

    @staticmethod
    def verify_sign(
        params: Dict[str, Any],
        secret_key: str,
        sign_type: str = 'md5'
    ) -> bool:
        """
        验证签名

        参数:
            params: 请求参数字典(包含 sign 字段)
            secret_key: 密钥
            sign_type: 签名类型('md5' 或 'sha256')

        返回:
            验证结果(True/False)
        """
        # 1. 获取原始签名
        original_sign = params.get('sign') or params.get('signature')
        if not original_sign:
            return False

        # 2. 重新计算签名
        if sign_type.lower() == 'sha256':
            calculated_sign = SignUtils.generate_sha256_sign(params, secret_key)
        else:
            calculated_sign = SignUtils.generate_md5_sign(params, secret_key)

        # 3. 对比签名(不区分大小写)
        return original_sign.upper() == calculated_sign.upper()

4.1.2 签名工具类的使用示例

# 示例1:基本 MD5 签名
from utils.sign_utils import SignUtils

params = {
    "order_id": "20240101001",
    "amount": "100.00",
    "user_id": "1001"
}
secret_key = "my_secret_key_123"

# 生成签名
sign = SignUtils.generate_md5_sign(params, secret_key)
print(f"MD5 签名: {sign}")

# 添加到参数中
params["sign"] = sign
print(f"带签名的参数: {params}")

# 示例2:SHA256 签名
sign_sha256 = SignUtils.generate_sha256_sign(params, secret_key)
print(f"SHA256 签名: {sign_sha256}")

# 示例3:带时间戳和随机数的签名
sign, final_params = SignUtils.generate_sign_with_timestamp(
    params,
    secret_key,
    sign_type='md5',
    add_timestamp=True,
    add_nonce=True
)
print(f"签名: {sign}")
print(f"最终参数: {final_params}")

# 示例4:验证签名
is_valid = SignUtils.verify_sign(final_params, secret_key, 'md5')
print(f"签名验证结果: {is_valid}")

4.2 创建支持签名的 HTTP 客户端

4.2.1 支持自动签名的 HTTP 客户端

# clients/signed_http_client.py
"""
支持自动签名的 HTTP 客户端
"""
import requests
from typing import Dict, Any, Optional
from utils.sign_utils import SignUtils

class SignedHttpClient:
    """支持签名的 HTTP 客户端"""

    def __init__(
        self,
        base_url: str,
        secret_key: Optional[str] = None,
        sign_config: Optional[Dict[str, Any]] = None
    ):
        """
        初始化签名 HTTP 客户端

        参数:
            base_url: 基础 URL
            secret_key: 密钥
            sign_config: 签名配置
                {
                    "sign_type": "md5",  # 签名类型
                    "auto_sign": True,  # 是否自动签名
                    "sign_exclude_keys": [],  # 签名排除的字段
                    "add_timestamp": True,  # 是否添加时间戳
                    "add_nonce": True,  # 是否添加随机数
                    "sign_field_name": "sign"  # 签名字段名
                }
        """
        self.base_url = base_url.rstrip('/')
        self.secret_key = secret_key
        self.session = requests.Session()

        # 默认签名配置
        self.sign_config = {
            "sign_type": "md5",
            "auto_sign": False,
            "sign_exclude_keys": [],
            "add_timestamp": False,
            "add_nonce": False,
            "sign_field_name": "sign"
        }
        if sign_config:
            self.sign_config.update(sign_config)

    def _add_sign(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """
        添加签名到参数

        参数:
            params: 原始参数字典

        返回:
            添加签名后的参数字典
        """
        if not self.secret_key:
            return params

        sign_type = self.sign_config.get("sign_type", "md5")
        exclude_keys = self.sign_config.get("sign_exclude_keys", [])
        add_timestamp = self.sign_config.get("add_timestamp", False)
        add_nonce = self.sign_config.get("add_nonce", False)
        sign_field_name = self.sign_config.get("sign_field_name", "sign")

        # 如果需要添加时间戳和随机数
        if add_timestamp or add_nonce:
            sign, final_params = SignUtils.generate_sign_with_timestamp(
                params,
                self.secret_key,
                sign_type=sign_type,
                add_timestamp=add_timestamp,
                add_nonce=add_nonce
            )
            return final_params
        else:
            # 普通签名
            if sign_type == "sha256":
                sign = SignUtils.generate_sha256_sign(
                    params, self.secret_key, exclude_keys
                )
            else:
                sign = SignUtils.generate_md5_sign(
                    params, self.secret_key, exclude_keys
                )

            params[sign_field_name] = sign
            return params

    def request(
        self,
        method: str,
        endpoint: str,
        params: Optional[Dict[str, Any]] = None,
        json_data: Optional[Dict[str, Any]] = None,
        add_sign: bool = False,
        **kwargs
    ) -> requests.Response:
        """
        发送请求

        参数:
            method: HTTP 方法
            endpoint: 接口路径
            params: URL 参数
            json_data: JSON 数据
            add_sign: 是否添加签名
            **kwargs: 其他 requests 参数

        返回:
            响应对象
        """
        url = f"{self.base_url}/{endpoint.lstrip('/')}"

        # 处理参数签名
        if params and (add_sign or self.sign_config.get("auto_sign", False)):
            params = self._add_sign(params)

        if json_data and (add_sign or self.sign_config.get("auto_sign", False)):
            json_data = self._add_sign(json_data)

        # 发送请求
        response = self.session.request(
            method=method.upper(),
            url=url,
            params=params,
            json=json_data,
            **kwargs
        )

        return response

    def get(
        self,
        endpoint: str,
        params: Optional[Dict[str, Any]] = None,
        add_sign: bool = False,
        **kwargs
    ) -> requests.Response:
        """GET 请求"""
        return self.request("GET", endpoint, params=params, add_sign=add_sign, **kwargs)

    def post(
        self,
        endpoint: str,
        json_data: Optional[Dict[str, Any]] = None,
        params: Optional[Dict[str, Any]] = None,
        add_sign: bool = False,
        **kwargs
    ) -> requests.Response:
        """POST 请求"""
        return self.request(
            "POST", endpoint, json_data=json_data,
            params=params, add_sign=add_sign, **kwargs
        )

    def put(
        self,
        endpoint: str,
        json_data: Optional[Dict[str, Any]] = None,
        add_sign: bool = False,
        **kwargs
    ) -> requests.Response:
        """PUT 请求"""
        return self.request(
            "PUT", endpoint, json_data=json_data, add_sign=add_sign, **kwargs
        )

    def delete(
        self,
        endpoint: str,
        add_sign: bool = False,
        **kwargs
    ) -> requests.Response:
        """DELETE 请求"""
        return self.request("DELETE", endpoint, add_sign=add_sign, **kwargs)

4.2.2 签名 HTTP 客户端的使用示例

# 示例1:基本使用
from clients.signed_http_client import SignedHttpClient

# 创建客户端
client = SignedHttpClient(
    base_url="https://api.example.com",
    secret_key="my_secret_key_123",
    sign_config={
        "sign_type": "md5",
        "auto_sign": False  # 手动控制签名
    }
)

# 发送带签名的请求
params = {
    "order_id": "20240101001",
    "amount": "100.00"
}

response = client.post("/api/payment", json_data=params, add_sign=True)
print(f"状态码: {response.status_code}")
print(f"响应: {response.json()}")

# 示例2:自动签名
client_auto = SignedHttpClient(
    base_url="https://api.example.com",
    secret_key="my_secret_key_123",
    sign_config={
        "sign_type": "md5",
        "auto_sign": True,  # 自动签名
        "add_timestamp": True,
        "add_nonce": True
    }
)

# 所有请求都会自动添加签名
response = client_auto.post("/api/payment", json_data=params)

5. Pytest 中的签名封装实战

5.1 创建 Pytest Fixture

5.1.1 签名工具 Fixture

# conftest.py
import pytest
from utils.sign_utils import SignUtils
from clients.signed_http_client import SignedHttpClient

@pytest.fixture(scope="session")
def sign_utils():
    """签名工具 Fixture"""
    return SignUtils

@pytest.fixture(scope="session")
def secret_key():
    """密钥 Fixture"""
    # 实际使用时,应该从配置文件或环境变量读取
    import os
    return os.getenv("SECRET_KEY", "my_secret_key_123")

@pytest.fixture(scope="session")
def signed_client(secret_key):
    """签名 HTTP 客户端 Fixture"""
    return SignedHttpClient(
        base_url="https://api.example.com",
        secret_key=secret_key,
        sign_config={
            "sign_type": "md5",
            "auto_sign": False,
            "add_timestamp": True,
            "add_nonce": True
        }
    )

5.2 测试用例示例

5.2.1 示例1:支付接口签名测试

# tests/test_payment_sign.py
import pytest
from utils.sign_utils import SignUtils

class TestPaymentSign:
    """支付接口签名测试"""

    secret_key = "payment_secret_key_123"

    def test_payment_with_md5_sign(self, sign_utils):
        """测试支付接口 - MD5 签名"""
        # 支付参数
        params = {
            "order_id": "ORDER20240101001",
            "amount": "199.00",
            "user_id": "1001",
            "payment_method": "alipay"
        }

        # 生成签名
        sign = sign_utils.generate_md5_sign(params, self.secret_key)

        # 添加签名
        params["sign"] = sign

        # 验证签名
        is_valid = sign_utils.verify_sign(params, self.secret_key, 'md5')
        assert is_valid, "签名验证失败"

        print(f"签名: {sign}")
        print(f"最终参数: {params}")

    def test_payment_with_sha256_sign(self, sign_utils):
        """测试支付接口 - SHA256 签名"""
        params = {
            "order_id": "ORDER20240101002",
            "amount": "299.00",
            "user_id": "1002"
        }

        # 生成 SHA256 签名
        sign = sign_utils.generate_sha256_sign(params, self.secret_key)
        params["sign"] = sign

        # 验证签名
        is_valid = sign_utils.verify_sign(params, self.secret_key, 'sha256')
        assert is_valid, "签名验证失败"

        print(f"SHA256 签名: {sign}")

    def test_payment_with_timestamp_nonce(self, sign_utils):
        """测试支付接口 - 带时间戳和随机数"""
        params = {
            "order_id": "ORDER20240101003",
            "amount": "399.00",
            "user_id": "1003"
        }

        # 生成带时间戳和随机数的签名
        sign, final_params = sign_utils.generate_sign_with_timestamp(
            params,
            self.secret_key,
            sign_type='md5',
            add_timestamp=True,
            add_nonce=True
        )

        # 验证签名
        is_valid = sign_utils.verify_sign(final_params, self.secret_key, 'md5')
        assert is_valid, "签名验证失败"

        print(f"签名: {sign}")
        print(f"时间戳: {final_params.get('timestamp')}")
        print(f"随机数: {final_params.get('nonce')}")
        print(f"最终参数: {final_params}")

5.2.2 示例2:使用签名客户端测试

# tests/test_api_with_sign.py
import pytest
from clients.signed_http_client import SignedHttpClient

class TestAPIWithSign:
    """使用签名客户端的 API 测试"""

    @pytest.fixture
    def api_client(self, secret_key):
        """API 客户端"""
        return SignedHttpClient(
            base_url="https://api.example.com",
            secret_key=secret_key,
            sign_config={
                "sign_type": "md5",
                "auto_sign": True,
                "add_timestamp": True,
                "add_nonce": True
            }
        )

    def test_payment_api(self, api_client):
        """测试支付接口"""
        data = {
            "order_id": "ORDER20240101001",
            "amount": "199.00",
            "user_id": "1001"
        }

        # 发送请求,会自动添加签名
        response = api_client.post("/api/payment", json_data=data)

        # 验证响应
        assert response.status_code == 200
        result = response.json()
        assert result["code"] == 0

        print(f"请求数据: {data}")
        print(f"响应: {result}")

    def test_user_update_api(self, api_client):
        """测试用户更新接口"""
        data = {
            "user_id": "1001",
            "nickname": "新昵称",
            "email": "newemail@example.com"
        }

        # 发送请求,会自动添加签名
        response = api_client.put("/api/user/update", json_data=data)

        assert response.status_code == 200
        result = response.json()
        assert result["code"] == 0

5.2.3 示例3:签名验证测试

# tests/test_sign_verification.py
import pytest
from utils.sign_utils import SignUtils

class TestSignVerification:
    """签名验证测试"""

    secret_key = "test_secret_key_123"

    def test_verify_correct_sign(self, sign_utils):
        """测试验证正确的签名"""
        params = {
            "order_id": "123",
            "amount": "100.00"
        }

        # 生成签名
        sign = sign_utils.generate_md5_sign(params, self.secret_key)
        params["sign"] = sign

        # 验证签名
        is_valid = sign_utils.verify_sign(params, self.secret_key, 'md5')
        assert is_valid, "正确的签名应该验证通过"

    def test_verify_wrong_sign(self, sign_utils):
        """测试验证错误的签名"""
        params = {
            "order_id": "123",
            "amount": "100.00",
            "sign": "WRONG_SIGN_12345"  # 错误的签名
        }

        # 验证签名
        is_valid = sign_utils.verify_sign(params, self.secret_key, 'md5')
        assert not is_valid, "错误的签名应该验证失败"

    def test_verify_tampered_params(self, sign_utils):
        """测试验证被篡改的参数"""
        params = {
            "order_id": "123",
            "amount": "100.00"
        }

        # 生成正确的签名
        sign = sign_utils.generate_md5_sign(params, self.secret_key)
        params["sign"] = sign

        # 篡改参数
        params["amount"] = "1.00"  # 修改金额

        # 验证签名(应该失败)
        is_valid = sign_utils.verify_sign(params, self.secret_key, 'md5')
        assert not is_valid, "参数被篡改后,签名验证应该失败"

    def test_verify_missing_sign(self, sign_utils):
        """测试验证缺少签名的参数"""
        params = {
            "order_id": "123",
            "amount": "100.00"
            # 没有 sign 字段
        }

        # 验证签名(应该失败)
        is_valid = sign_utils.verify_sign(params, self.secret_key, 'md5')
        assert not is_valid, "缺少签名时,验证应该失败"

6. 实际应用场景详解

6.1 场景1:电商支付接口

6.1.1 需求分析

电商支付接口需要:

  1. 对支付参数进行 MD5 签名
  2. 添加时间戳防止重放攻击
  3. 添加随机数确保唯一性
  4. 验证签名确保数据完整性

6.1.2 实现代码

# tests/test_ecommerce_payment.py
import pytest
from utils.sign_utils import SignUtils
import requests

class TestEcommercePayment:
    """电商支付接口测试"""

    base_url = "https://api.ecommerce.com"
    secret_key = "ecommerce_secret_key_123"

    def test_payment_flow(self, sign_utils):
        """完整的支付流程测试"""
        # 步骤1:准备支付参数
        payment_params = {
            "order_id": "ORDER20240101001",
            "amount": "199.00",
            "user_id": "1001",
            "payment_method": "alipay",
            "product_id": "PROD001"
        }

        # 步骤2:生成带时间戳和随机数的签名
        sign, final_params = sign_utils.generate_sign_with_timestamp(
            payment_params,
            self.secret_key,
            sign_type='md5',
            add_timestamp=True,
            add_nonce=True
        )

        print(f"支付参数: {final_params}")
        print(f"签名: {sign}")

        # 步骤3:验证签名
        is_valid = sign_utils.verify_sign(final_params, self.secret_key, 'md5')
        assert is_valid, "签名验证失败"

        # 步骤4:发送支付请求
        url = f"{self.base_url}/api/payment"
        response = requests.post(url, json=final_params)

        # 步骤5:验证响应
        assert response.status_code == 200
        result = response.json()
        assert result["code"] == 0
        assert result["data"]["status"] == "success"

        print(f"支付成功!订单号: {final_params['order_id']}")

    def test_payment_sign_verification(self, sign_utils):
        """测试支付签名验证"""
        # 模拟服务器收到的请求
        received_params = {
            "order_id": "ORDER20240101001",
            "amount": "199.00",
            "user_id": "1001",
            "timestamp": "1704067200",
            "nonce": "abc123def456",
            "sign": "A8F5F167F44F4964E6C998DEE827110C"
        }

        # 验证签名
        is_valid = sign_utils.verify_sign(received_params, self.secret_key, 'md5')

        if is_valid:
            print("签名验证通过,处理支付请求")
        else:
            print("签名验证失败,拒绝支付请求")

        # 注意:这个签名是示例,实际验证可能会失败
        # 但演示了如何验证签名

6.2 场景2:用户信息修改接口

6.2.1 需求分析

用户信息修改接口需要:

  1. 对修改参数进行 SHA256 签名
  2. 验证签名确保请求合法性
  3. 防止参数被篡改

6.2.2 实现代码

# tests/test_user_update_sign.py
import pytest
from utils.sign_utils import SignUtils
import requests

class TestUserUpdateSign:
    """用户信息修改接口签名测试"""

    base_url = "https://api.example.com"
    secret_key = "user_api_secret_key_123"

    def test_update_user_info(self, sign_utils):
        """测试更新用户信息"""
        # 用户信息参数
        update_params = {
            "user_id": "1001",
            "nickname": "新昵称",
            "email": "newemail@example.com",
            "phone": "13800138000"
        }

        # 生成 SHA256 签名
        sign = sign_utils.generate_sha256_sign(update_params, self.secret_key)
        update_params["sign"] = sign

        # 验证签名
        is_valid = sign_utils.verify_sign(update_params, self.secret_key, 'sha256')
        assert is_valid, "签名验证失败"

        # 发送更新请求
        url = f"{self.base_url}/api/user/update"
        response = requests.put(url, json=update_params)

        assert response.status_code == 200
        result = response.json()
        assert result["code"] == 0

        print(f"用户信息更新成功")
        print(f"签名: {sign}")

    def test_update_user_info_tampered(self, sign_utils):
        """测试参数被篡改的情况"""
        update_params = {
            "user_id": "1001",
            "nickname": "新昵称",
            "email": "newemail@example.com"
        }

        # 生成正确的签名
        sign = sign_utils.generate_sha256_sign(update_params, self.secret_key)
        update_params["sign"] = sign

        # 篡改参数(模拟攻击)
        update_params["user_id"] = "9999"  # 修改用户ID

        # 验证签名(应该失败)
        is_valid = sign_utils.verify_sign(update_params, self.secret_key, 'sha256')
        assert not is_valid, "参数被篡改后,签名验证应该失败"

        print("参数被篡改,签名验证失败(符合预期)")

6.3 场景3:第三方 API 调用

6.3.1 需求分析

调用第三方 API 需要:

  1. 使用 API Key 和 Secret Key
  2. 对请求参数进行签名
  3. 添加时间戳
  4. 验证响应签名(如果第三方返回签名)

6.3.2 实现代码

# tests/test_third_party_api.py
import pytest
from utils.sign_utils import SignUtils
import requests

class TestThirdPartyAPI:
    """第三方 API 调用测试"""

    api_key = "your_api_key_123"
    secret_key = "your_secret_key_456"
    base_url = "https://api.thirdparty.com"

    def test_call_third_party_api(self, sign_utils):
        """调用第三方 API"""
        # 请求参数
        params = {
            "api_key": self.api_key,
            "action": "get_data",
            "params": {
                "id": 123,
                "type": "user"
            }
        }

        # 将嵌套字典转换为字符串(根据第三方API要求)
        import json
        params["params"] = json.dumps(params["params"])

        # 生成签名
        sign = sign_utils.generate_md5_sign(params, self.secret_key)
        params["sign"] = sign

        # 添加时间戳
        import time
        params["timestamp"] = str(int(time.time()))

        # 发送请求
        url = f"{self.base_url}/api/v1/execute"
        response = requests.post(url, json=params)

        assert response.status_code == 200
        result = response.json()

        # 如果第三方返回了签名,可以验证
        if "sign" in result:
            # 验证响应签名(需要根据第三方文档实现)
            pass

        print(f"第三方 API 调用成功")
        print(f"响应: {result}")

7. 常见问题和解决方案

7.1 问题1:签名验证总是失败

7.1.1 问题描述

生成的签名总是验证失败,不知道哪里出了问题。

7.1.2 可能的原因

  1. 参数排序不一致:客户端和服务器使用的排序规则不同
  2. 空值处理不一致:空值是否参与签名
  3. 密钥不一致:客户端和服务器使用的密钥不同
  4. 编码问题:字符串编码不一致
  5. 签名算法不一致:使用了不同的签名算法
  6. 密钥位置不同:密钥拼接位置不同

7.1.3 解决方案

def debug_sign_generation(params, secret_key):
    """调试签名生成过程"""
    print("=" * 60)
    print("签名生成调试信息")
    print("=" * 60)

    # 1. 检查原始参数
    print(f"n1. 原始参数:")
    print(f"   {params}")

    # 2. 移除签名字段
    sign_params = {k: v for k, v in params.items() if k != 'sign'}
    print(f"n2. 移除签名字段后:")
    print(f"   {sign_params}")

    # 3. 检查空值
    empty_keys = [k for k, v in sign_params.items() if v is None or v == '']
    if empty_keys:
        print(f"n3. 发现空值字段: {empty_keys}")
        print(f"   注意:空值是否参与签名需要确认")

    # 4. 排序
    sorted_params = sorted(sign_params.items())
    print(f"n4. 按键名排序后:")
    for k, v in sorted_params:
        print(f"   {k} = {v}")

    # 5. 拼接字符串
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])
    print(f"n5. 拼接字符串:")
    print(f"   {sign_string}")

    # 6. 添加密钥
    sign_string_with_key = sign_string + f"&key={secret_key}"
    print(f"n6. 添加密钥后:")
    print(f"   {sign_string_with_key}")

    # 7. 检查编码
    sign_bytes = sign_string_with_key.encode('utf-8')
    print(f"n7. UTF-8 编码后:")
    print(f"   {sign_bytes}")

    # 8. 计算签名
    import hashlib
    sign = hashlib.md5(sign_bytes).hexdigest().upper()
    print(f"n8. MD5 签名:")
    print(f"   {sign}")
    print("=" * 60)

    return sign

# 使用示例
params = {
    "order_id": "123",
    "amount": "100.00",
    "user_id": ""
}
secret_key = "my_key"

sign = debug_sign_generation(params, secret_key)

7.2 问题2:时间戳格式不一致

7.2.1 问题描述

服务器要求的时间戳格式与生成的不一致。

7.2.2 解决方案

import time
from datetime import datetime

def get_timestamp(format_type="unix"):
    """
    获取不同格式的时间戳

    参数:
        format_type: 格式类型
            - "unix": Unix 时间戳(秒)
            - "unix_ms": Unix 时间戳(毫秒)
            - "datetime": 日期时间字符串 "YYYY-MM-DD HH:MM:SS"
            - "date": 日期字符串 "YYYY-MM-DD"
            - "time": 时间字符串 "HH:MM:SS"
    """
    if format_type == "unix":
        return str(int(time.time()))
    elif format_type == "unix_ms":
        return str(int(time.time() * 1000))
    elif format_type == "datetime":
        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    elif format_type == "date":
        return datetime.now().strftime("%Y-%m-%d")
    elif format_type == "time":
        return datetime.now().strftime("%H:%M:%S")
    else:
        return str(int(time.time()))

# 使用示例
print(f"Unix 时间戳(秒): {get_timestamp('unix')}")
print(f"Unix 时间戳(毫秒): {get_timestamp('unix_ms')}")
print(f"日期时间字符串: {get_timestamp('datetime')}")
print(f"日期字符串: {get_timestamp('date')}")
print(f"时间字符串: {get_timestamp('time')}")

7.3 问题3:嵌套参数的处理

7.3.1 问题描述

请求参数中包含嵌套字典或列表,不知道如何参与签名。

7.3.2 解决方案

import json
import hashlib

def sign_with_nested_params(params, secret_key):
    """
    处理嵌套参数的签名

    参数:
        params: 可能包含嵌套字典或列表的参数
        secret_key: 密钥
    """
    def flatten_params(data, parent_key='', sep='_'):
        """展平嵌套参数"""
        items = []
        if isinstance(data, dict):
            for k, v in data.items():
                new_key = f"{parent_key}{sep}{k}" if parent_key else k
                if isinstance(v, (dict, list)):
                    items.extend(flatten_params(v, new_key, sep=sep).items())
                else:
                    items.append((new_key, v))
        elif isinstance(data, list):
            for i, v in enumerate(data):
                new_key = f"{parent_key}{sep}{i}" if parent_key else str(i)
                if isinstance(v, (dict, list)):
                    items.extend(flatten_params(v, new_key, sep=sep).items())
                else:
                    items.append((new_key, v))
        else:
            return {parent_key: data} if parent_key else {}

        return dict(items)

    # 方式1:展平嵌套参数
    flat_params = flatten_params(params)
    print(f"展平后的参数: {flat_params}")

    # 方式2:将嵌套结构转为 JSON 字符串
    # 根据实际需求选择方式
    sign_params = {k: v for k, v in params.items() if k != 'sign'}

    # 将嵌套字典转为 JSON 字符串
    for k, v in sign_params.items():
        if isinstance(v, (dict, list)):
            sign_params[k] = json.dumps(v, ensure_ascii=False, sort_keys=True)

    # 排序和拼接
    sorted_params = sorted(sign_params.items())
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])
    sign_string += f"&key={secret_key}"

    # 计算签名
    sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()

    return sign

# 使用示例
params = {
    "order_id": "123",
    "amount": "100.00",
    "items": [
        {"id": 1, "name": "商品1"},
        {"id": 2, "name": "商品2"}
    ],
    "user": {
        "id": 1001,
        "name": "张三"
    }
}
secret_key = "my_key"

sign = sign_with_nested_params(params, secret_key)
print(f"签名: {sign}")

7.4 问题4:URL 参数和 Body 参数的签名

7.4.1 问题描述

有些接口的参数在 URL 中,有些在 Body 中,不知道如何统一签名。

7.4.2 解决方案

def sign_url_and_body(url_params, body_params, secret_key):
    """
    对 URL 参数和 Body 参数统一签名

    参数:
        url_params: URL 参数字典
        body_params: Body 参数字典
        secret_key: 密钥
    """
    # 合并所有参数
    all_params = {}

    # 添加 URL 参数
    if url_params:
        all_params.update(url_params)

    # 添加 Body 参数
    if body_params:
        all_params.update(body_params)

    # 移除签名字段
    sign_params = {k: v for k, v in all_params.items() if k != 'sign'}

    # 排序和拼接
    sorted_params = sorted(sign_params.items())
    sign_string = "&".join([f"{k}={v}" for k, v in sorted_params])
    sign_string += f"&key={secret_key}"

    # 计算签名
    import hashlib
    sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()

    return sign

# 使用示例
url_params = {
    "user_id": "1001",
    "action": "get_info"
}

body_params = {
    "name": "张三",
    "email": "zhangsan@example.com"
}

secret_key = "my_key"

sign = sign_url_and_body(url_params, body_params, secret_key)
print(f"统一签名: {sign}")

# 可以将签名添加到 URL 参数或 Body 参数中
url_params["sign"] = sign
# 或
body_params["sign"] = sign

8. 最佳实践

8.1 密钥管理

8.1.1 不要硬编码密钥

# ❌ 错误做法
secret_key = "my_secret_key_123"

# ✅ 正确做法:使用环境变量
import os
from dotenv import load_dotenv

load_dotenv()
secret_key = os.getenv("SECRET_KEY")

# ✅ 或从配置文件读取
import yaml
with open("config.yaml", "r") as f:
    config = yaml.safe_load(f)
    secret_key = config["secret_key"]

8.1.2 使用配置文件

创建 config/sign_config.yaml

# 签名配置
sign:
  secret_key: "${SECRET_KEY}"  # 从环境变量读取
  sign_type: "md5"
  auto_sign: true
  add_timestamp: true
  add_nonce: true
  sign_field_name: "sign"
  exclude_keys:
    - "sign"
    - "signature"

8.2 错误处理

def safe_generate_sign(params, secret_key, sign_type='md5'):
    """安全的签名生成(带错误处理)"""
    try:
        if sign_type == 'md5':
            return SignUtils.generate_md5_sign(params, secret_key)
        elif sign_type == 'sha256':
            return SignUtils.generate_sha256_sign(params, secret_key)
        else:
            raise ValueError(f"不支持的签名类型: {sign_type}")
    except Exception as e:
        print(f"签名生成失败: {e}")
        raise

8.3 日志记录

import logging

logger = logging.getLogger(__name__)

def sign_with_log(params, secret_key, sign_type='md5'):
    """带日志记录的签名生成"""
    logger.info(f"开始生成签名,类型: {sign_type}, 参数数量: {len(params)}")

    try:
        if sign_type == 'md5':
            sign = SignUtils.generate_md5_sign(params, secret_key)
        else:
            sign = SignUtils.generate_sha256_sign(params, secret_key)

        logger.info(f"签名生成成功,签名: {sign[:10]}...")
        return sign
    except Exception as e:
        logger.error(f"签名生成失败: {e}", exc_info=True)
        raise

8.4 单元测试

# tests/test_sign_utils.py
import pytest
from utils.sign_utils import SignUtils

class TestSignUtils:
    """签名工具类测试"""

    secret_key = "test_secret_key_123"

    def test_md5_sign(self):
        """测试 MD5 签名"""
        params = {"order_id": "123", "amount": "100.00"}
        sign = SignUtils.generate_md5_sign(params, self.secret_key)

        assert len(sign) == 32  # MD5 结果是32位16进制
        assert sign.isupper()  # 默认是大写

        # 验证签名
        params["sign"] = sign
        assert SignUtils.verify_sign(params, self.secret_key, 'md5')

    def test_sha256_sign(self):
        """测试 SHA256 签名"""
        params = {"order_id": "123", "amount": "100.00"}
        sign = SignUtils.generate_sha256_sign(params, self.secret_key)

        assert len(sign) == 64  # SHA256 结果是64位16进制

        # 验证签名
        params["sign"] = sign
        assert SignUtils.verify_sign(params, self.secret_key, 'sha256')

    def test_sign_with_timestamp(self):
        """测试带时间戳的签名"""
        params = {"order_id": "123"}
        sign, final_params = SignUtils.generate_sign_with_timestamp(
            params, self.secret_key, add_timestamp=True, add_nonce=True
        )

        assert "timestamp" in final_params
        assert "nonce" in final_params
        assert "sign" in final_params
        assert final_params["sign"] == sign

        # 验证签名
        assert SignUtils.verify_sign(final_params, self.secret_key, 'md5')

    def test_verify_wrong_sign(self):
        """测试验证错误的签名"""
        params = {
            "order_id": "123",
            "sign": "WRONG_SIGN"
        }

        assert not SignUtils.verify_sign(params, self.secret_key, 'md5')

9. 完整实战案例

9.1 案例:完整的支付接口测试

# tests/test_complete_payment.py
import pytest
import requests
from utils.sign_utils import SignUtils

class TestCompletePayment:
    """完整的支付接口测试"""

    base_url = "https://api.payment.com"
    secret_key = "payment_secret_key_123"

    @pytest.fixture
    def payment_params(self):
        """支付参数 Fixture"""
        return {
            "order_id": "ORDER20240101001",
            "amount": "199.00",
            "user_id": "1001",
            "payment_method": "alipay",
            "product_id": "PROD001",
            "product_name": "测试商品"
        }

    def test_payment_with_sign(self, payment_params, sign_utils):
        """测试支付接口 - 带签名"""
        # 步骤1:生成签名
        sign, final_params = sign_utils.generate_sign_with_timestamp(
            payment_params,
            self.secret_key,
            sign_type='md5',
            add_timestamp=True,
            add_nonce=True
        )

        # 步骤2:验证签名
        is_valid = sign_utils.verify_sign(final_params, self.secret_key, 'md5')
        assert is_valid, "签名验证失败"

        # 步骤3:发送支付请求
        url = f"{self.base_url}/api/payment"
        response = requests.post(url, json=final_params)

        # 步骤4:验证响应
        assert response.status_code == 200
        result = response.json()
        assert result["code"] == 0
        assert result["data"]["status"] == "success"

        print(f"支付成功!")
        print(f"订单号: {final_params['order_id']}")
        print(f"签名: {sign}")

    def test_payment_sign_tampered(self, payment_params, sign_utils):
        """测试支付接口 - 参数被篡改"""
        # 生成正确的签名
        sign, final_params = sign_utils.generate_sign_with_timestamp(
            payment_params,
            self.secret_key,
            sign_type='md5'
        )

        # 篡改参数(模拟攻击)
        final_params["amount"] = "1.00"  # 修改金额

        # 验证签名(应该失败)
        is_valid = sign_utils.verify_sign(final_params, self.secret_key, 'md5')
        assert not is_valid, "参数被篡改后,签名验证应该失败"

        # 尝试发送请求(应该被服务器拒绝)
        url = f"{self.base_url}/api/payment"
        response = requests.post(url, json=final_params)

        # 服务器应该拒绝请求
        assert response.status_code != 200 or response.json()["code"] != 0

        print("参数被篡改,请求被拒绝(符合预期)")

    @pytest.mark.parametrize("sign_type", ["md5", "sha256"])
    def test_payment_different_sign_types(self, payment_params, sign_utils, sign_type):
        """测试不同签名类型的支付接口"""
        # 根据签名类型生成签名
        if sign_type == "sha256":
            sign = sign_utils.generate_sha256_sign(payment_params, self.secret_key)
        else:
            sign = sign_utils.generate_md5_sign(payment_params, self.secret_key)

        payment_params["sign"] = sign

        # 验证签名
        is_valid = sign_utils.verify_sign(payment_params, self.secret_key, sign_type)
        assert is_valid, f"{sign_type} 签名验证失败"

        print(f"{sign_type} 签名验证通过")
        print(f"签名: {sign}")

10. 总结

10.1 核心要点

  1. 理解签名原理:签名是验证数据完整性和来源的机制
  2. 掌握签名算法:MD5、SHA256 等常用算法
  3. 规范签名流程:参数排序、拼接、添加密钥、计算签名
  4. 封装签名工具:创建可复用的签名工具类
  5. 完善错误处理:处理各种异常情况
  6. 安全实践:密钥管理、防止重放攻击

10.2 学习路径

  1. 基础阶段:理解签名的概念和原理
  2. 实践阶段:实现基本的签名生成和验证
  3. 封装阶段:封装成工具类,方便复用
  4. 应用阶段:在实际项目中应用签名机制
  5. 优化阶段:完善错误处理、日志记录、单元测试

10.3 扩展学习

  • JWT Token:学习 JWT 的签名机制
  • OAuth 2.0:了解 OAuth 2.0 的签名流程
  • 数字签名:深入学习数字签名的原理
  • HTTPS:理解 HTTPS 中的签名机制

11. 附录

11.1 依赖安装

# 基础依赖
pip install requests pytest pyyaml

# 环境变量管理
pip install python-dotenv

11.2 完整项目结构

project/
├── utils/
│   ├── __init__.py
│   └── sign_utils.py          # 签名工具类
├── clients/
│   ├── __init__.py
│   └── signed_http_client.py  # 签名 HTTP 客户端
├── tests/
│   ├── __init__.py
│   ├── conftest.py           # Pytest 配置
│   ├── test_payment_sign.py
│   ├── test_api_with_sign.py
│   └── test_sign_verification.py
├── config/
│   └── sign_config.yaml      # 签名配置
├── .env                      # 环境变量(不提交到 Git)
├── requirements.txt
└── README.md

11.3 参考资源


祝学习愉快!如有问题,欢迎查阅文档或寻求帮助。 🎉

发表评论