本文目录导读:
- 核心思想
- 场景一:最常见 — HMAC-SHA256 验签(对称密钥)
- 场景二:RSA 非对称验签(安全性更高)
- 场景三:较老的 Hash 签名(MD5 或 SHA1)
- 系统级防御:防止重放攻击
- 实战核对清单
- 调试技巧
回调验签(Callback Signature Verification)是 Webhook 或 API 回调中常用的安全机制,其核心目的是验证收到的回调请求确实来自合法的服务器,且在传输过程中未被篡改。
实现思路通常如下(以最常见的 HMAC-SHA256 和 RSA 为例):
核心思想
- 约定密钥:双方(你的服务器和第三方服务)预先约定一个密钥(Secret Key)或公钥/私钥对。
- 生成签名:第三方服务将请求参数(通常是 JSON 体) 按照规则排序后,使用 Hash 算法(如 SHA-256)加上密钥生成一个签名(
sign、signature字段)。 - 验签:你的服务器收到回调后,用收到的参数数据,用同样的规则和同样的密钥重新计算签名,然后与回调请求中携带的
sign字段进行对比。
最常见 — HMAC-SHA256 验签(对称密钥)
多数回调(如微信支付、支付宝、GitHub Webhooks)使用 HMAC,验证签名通常在请求体(Body) 或请求头(Header) 中。
代码示例(Python + Flask,适用于大多数场景)
import hmac
import hashlib
import json
from flask import Flask, request, abort
app = Flask(__name__)
# 1. 预先与第三方约定的密钥(务必从环境变量读取,不要硬编码)
SECRET_KEY = b'your_shared_secret_key_here'
def verify_signature(request_body_bytes, signature_header):
"""
使用 HMAC-SHA256 验证签名
:param request_body_bytes: 接收到的原始请求体字节数据
:param signature_header: 请求头或请求体中的签名字符串
:return: True 或 False
"""
if not signature_header:
return False
# 计算期望的签名(HMAC-SHA256)
expected_signature = hmac.new(
SECRET_KEY,
request_body_bytes,
hashlib.sha256
).hexdigest() # 通常是 hex 格式,也可能是 base64
# 安全比较:使用 hmac.compare_digest 防止时序攻击
return hmac.compare_digest(expected_signature, signature_header)
@app.route('/callback', methods=['POST'])
def receive_callback():
# 获取原始请求体(必须是字节,不能修改顺序或去空格)
request_body = request.get_data()
# 获取签名(通常在请求头中,如 X-Signature)
client_signature = request.headers.get('X-Signature')
# 亦或在请求体中(如 JSON 里的 'sign' 字段)
# data = request.get_json()
# client_signature = data.pop('sign', '')
if not verify_signature(request_body, client_signature):
abort(403, description="Signature verification failed")
# 验签成功,处理业务
data = request.get_json()
print(f"Received valid callback: {data}")
return {"status": "ok"}, 200
if __name__ == '__main__':
app.run(port=5000)
关键点:
- 必须使用
request.get_data()获取原始字节,因为 JSON 解析会改变空格、顺序。 - 签名比较必须使用
hmac.compare_digest防止时序攻击。 - 密钥应从环境变量读取,不要硬编码。
RSA 非对称验签(安全性更高)
常用于银行支付、金融类 API,第三方使用私钥签名,你使用公钥验签。
代码示例(Python)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import serialization
import base64
# 1. 预存第三方公钥(从证书文件或字符串加载)
PUBLIC_KEY_STR = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD...
-----END PUBLIC KEY-----
"""
public_key = serialization.load_pem_public_key(PUBLIC_KEY_STR.encode())
def rsa_verify_signature(data_bytes, signature_base64):
"""
RSA SHA256 验签
:param data_bytes: 原始请求体数据(通常需按规则排序拼接,如按字母排序后拼接键值对)
:param signature_base64: 签名(Base64 编码)
:return: True/False
"""
try:
signature = base64.b64decode(signature_base64)
public_key.verify(
signature,
data_bytes,
padding.PKCS1v15(),
hashes.SHA256()
)
return True
except Exception as e:
print(f"RSA verification failed: {e}")
return False
常见排序规则示例(对接支付宝时常用):
def build_sign_string(data: dict) -> str:
# 1. 移除 sign 字段
sorted_keys = sorted(data.keys())
sorted_items = [f"{k}={data[k]}" for k in sorted_keys if data[k] != '' and k != 'sign']
return "&".join(sorted_items) + "&key=" + YOUR_SECRET
较老的 Hash 签名(MD5 或 SHA1)
仍有一些 API 使用(如部分支付回调),这类安全性较低,但实现简单。
import hashlib
def md5_verify(sign_str, provided_sign):
# 通常需要先拼接字符串:params + secret_key
expected_sign = hashlib.md5(sign_str.encode()).hexdigest()
return expected_sign == provided_sign
系统级防御:防止重放攻击
仅靠签名算法无法防止重放攻击(攻击者记录一个合法请求并反复发送)。
常见手段(验签时需同时检查):
- 检查时间戳:
timestamp = int(request.headers.get('X-Timestamp', 0)) if abs(time.time() - timestamp) > 300: # 5分钟窗口 abort(403, "Request expired") - 检查 Nonce(一次性随机数):
- 将收到的
nonce存入 Redis(带过期时间),若已存在则拒绝。
- 将收到的
实战核对清单
| 检查项 | 正确做法 | 常见错误 |
|---|---|---|
| 数据源 | 使用 request.get_data() 原始字节 |
使用 request.json(会修改空格和顺序) |
| 签名位置 | 根据文档确定(Header / Body / URL 参数) | 从错误位置取签名值 |
| 签名格式 | 确认是 hex(小写)还是 base64 | 格式不匹配导致比对失败 |
| 签名算法 | 明确 HMAC / RSA / MD5 | 算法混淆 |
| 密钥来源 | 环境变量或密钥管理服务 | 硬编码在代码里 |
| 安全比较 | hmac.compare_digest |
使用 (存在时序攻击风险) |
| 重放防护 | 校验时间戳 + Nonce | 只验签,不防重放 |
调试技巧
如果验签一直失败,可以:
- 打印原始数据:
print(repr(request.get_data()))检查是否有意外换行或空格。 - 使用第三方 SDK:如支付宝、微信支付等通常提供官方的验签 SDK 和沙箱环境,建议优先使用。
- 对比日志:将收到的签名值和你自己计算出的签名值打印出来,逐字符对比。
如果你能告诉我你对接的是哪个具体的 API(如微信支付、GitHub、Alipay 等),我可以给出更精确的代码示例。