Pytest 接口签名封装详解 – 新手完整教程
1. 什么是接口签名
1.1 接口签名的基本概念
接口签名(API Signature)是一种用于验证接口请求合法性和完整性的安全机制。它通过对请求参数进行加密计算,生成一个唯一的签名字符串,服务器通过验证这个签名来判断请求是否合法、是否被篡改。
1.2 为什么需要接口签名
想象一下,如果你在电商网站购买商品:
没有签名的情况:
客户端发送:我要购买商品A,价格100元
服务器收到:我要购买商品A,价格100元
有签名的情况:
客户端发送:
- 商品ID: A
- 价格: 100元
- 签名: a8f5f167f44f4964e6c998dee827110c(通过参数+密钥计算得出)
服务器收到后:
1. 使用相同规则计算签名
2. 对比签名是否一致
3. 一致则处理请求,不一致则拒绝
接口签名的主要作用:
- 防止数据篡改:如果有人修改了请求参数(比如把价格从100改成1),签名就会不匹配,请求会被拒绝
- 身份验证:只有拥有正确密钥的客户端才能生成正确的签名
- 防止重放攻击:通过时间戳和随机数,确保每个请求都是唯一的
- 数据完整性:确保数据在传输过程中没有被修改
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×tamp=1704067200
步骤5:添加密钥后
amount=100.00&order_id=20240101001×tamp=1704067200&key=my_secret_key_123
步骤6:计算 MD5
原始字节: b'amount=100.00&order_id=20240101001×tamp=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 需求分析
电商支付接口需要:
- 对支付参数进行 MD5 签名
- 添加时间戳防止重放攻击
- 添加随机数确保唯一性
- 验证签名确保数据完整性
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 需求分析
用户信息修改接口需要:
- 对修改参数进行 SHA256 签名
- 验证签名确保请求合法性
- 防止参数被篡改
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 需要:
- 使用 API Key 和 Secret Key
- 对请求参数进行签名
- 添加时间戳
- 验证响应签名(如果第三方返回签名)
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 可能的原因
- 参数排序不一致:客户端和服务器使用的排序规则不同
- 空值处理不一致:空值是否参与签名
- 密钥不一致:客户端和服务器使用的密钥不同
- 编码问题:字符串编码不一致
- 签名算法不一致:使用了不同的签名算法
- 密钥位置不同:密钥拼接位置不同
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 核心要点
- 理解签名原理:签名是验证数据完整性和来源的机制
- 掌握签名算法:MD5、SHA256 等常用算法
- 规范签名流程:参数排序、拼接、添加密钥、计算签名
- 封装签名工具:创建可复用的签名工具类
- 完善错误处理:处理各种异常情况
- 安全实践:密钥管理、防止重放攻击
10.2 学习路径
- 基础阶段:理解签名的概念和原理
- 实践阶段:实现基本的签名生成和验证
- 封装阶段:封装成工具类,方便复用
- 应用阶段:在实际项目中应用签名机制
- 优化阶段:完善错误处理、日志记录、单元测试
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 参考资源
祝学习愉快!如有问题,欢迎查阅文档或寻求帮助。 🎉