回调验签怎么代码实现?

访客 网络编程 3

本文目录导读:

  1. 核心思想
  2. 场景一:最常见 — HMAC-SHA256 验签(对称密钥)
  3. 场景二:RSA 非对称验签(安全性更高)
  4. 场景三:较老的 Hash 签名(MD5 或 SHA1)
  5. 系统级防御:防止重放攻击
  6. 实战核对清单
  7. 调试技巧

回调验签(Callback Signature Verification)是 Webhook 或 API 回调中常用的安全机制,其核心目的是验证收到的回调请求确实来自合法的服务器,且在传输过程中未被篡改

实现思路通常如下(以最常见的 HMAC-SHA256RSA 为例):


核心思想

  1. 约定密钥:双方(你的服务器和第三方服务)预先约定一个密钥(Secret Key)或公钥/私钥对。
  2. 生成签名:第三方服务将请求参数(通常是 JSON 体) 按照规则排序后,使用 Hash 算法(如 SHA-256)加上密钥生成一个签名(signsignature 字段)。
  3. 验签:你的服务器收到回调后,用收到的参数数据,用同样的规则和同样的密钥重新计算签名,然后与回调请求中携带的 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

系统级防御:防止重放攻击

仅靠签名算法无法防止重放攻击(攻击者记录一个合法请求并反复发送)。

常见手段(验签时需同时检查):

  1. 检查时间戳
    timestamp = int(request.headers.get('X-Timestamp', 0))
    if abs(time.time() - timestamp) > 300:  # 5分钟窗口
        abort(403, "Request expired")
  2. 检查 Nonce(一次性随机数)
    • 将收到的 nonce 存入 Redis(带过期时间),若已存在则拒绝。

实战核对清单

检查项 正确做法 常见错误
数据源 使用 request.get_data() 原始字节 使用 request.json(会修改空格和顺序)
签名位置 根据文档确定(Header / Body / URL 参数) 从错误位置取签名值
签名格式 确认是 hex(小写)还是 base64 格式不匹配导致比对失败
签名算法 明确 HMAC / RSA / MD5 算法混淆
密钥来源 环境变量或密钥管理服务 硬编码在代码里
安全比较 hmac.compare_digest 使用 (存在时序攻击风险)
重放防护 校验时间戳 + Nonce 只验签,不防重放

调试技巧

如果验签一直失败,可以:

  1. 打印原始数据print(repr(request.get_data())) 检查是否有意外换行或空格。
  2. 使用第三方 SDK:如支付宝、微信支付等通常提供官方的验签 SDK 和沙箱环境,建议优先使用。
  3. 对比日志:将收到的签名值和你自己计算出的签名值打印出来,逐字符对比。

如果你能告诉我你对接的是哪个具体的 API(如微信支付、GitHub、Alipay 等),我可以给出更精确的代码示例。

标签: 验签 回调

抱歉,评论功能暂时关闭!