为什么支付 API 需要加密与签名?
在支付系统中,API 接口直接暴露在公网,面临三类核心安全威胁:数据窃听(敏感信息被截获)、数据篡改(请求参数被中间人修改)、重放攻击(攻击者重复发送合法请求)。仅靠 HTTPS 是不够的——HTTPS 保护传输层,但无法防止应用层的重放和参数篡改。因此,必须在应用层实现端到端的加密与签名机制。
本文从零讲解支付 API 的完整安全方案,涵盖 RSA 非对称加密、AES 对称加密、HMAC-SHA256 签名、时间戳 Nonce 防重放,以及我们源码商城转卡码系统中实际使用的混合加密体系。
一、加密体系设计总览
一个成熟的支付 API 安全体系通常采用混合加密方案:
- AES-256-GCM:对称加密,加密请求/响应体,速度快
- RSA-2048:非对称加密,用于交换 AES 密钥
- HMAC-SHA256:签名,验证数据完整性和来源
- Timestamp + Nonce:防重放攻击
流程图如下:
# 加密流程(客户端 → 服务端) 1. 客户端生成随机 AES_KEY + IV 2. 用服务端 RSA 公钥加密 AES_KEY → encrypted_key 3. 用 AES_KEY 加密请求体 → encrypted_data 4. 拼接 timestamp + nonce + encrypted_data 计算 HMAC-SHA256 → sign 5. 发送 {encrypted_key, encrypted_data, sign, timestamp, nonce} # 解密流程(服务端接收) 1. 校验 timestamp 是否在 5 分钟内(防重放) 2. 检查 nonce 是否已使用(防重复) 3. 用 AES_KEY 解密 encrypted_data 4. 用服务端 RSA 私钥解密得到 AES_KEY 5. 验证 HMAC-SHA256 签名
二、RSA 密钥对生成与管理
首先,我们需要生成 2048 位的 RSA 密钥对:
# 生成 RSA 私钥(2048 位) openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 # 从私钥导出公钥 openssl pkey -in private_key.pem -pubout -out public_key.pem # 查看密钥信息 openssl pkey -in private_key.pem -text -noout | head -5
生产环境中,私钥必须安全存储——建议使用 Vault、AWS KMS 或 HashiCorp Consul 的密钥管理服务,永远不要将私钥硬编码在代码仓库中。
Python 中的 RSA 加解密实现
# crypto_rsa.py — RSA 加密/解密工具 from cryptography.hazmat.primitives import hashes, padding from cryptography.hazmat.primitives.asymmetric import rsa, padding as asym_padding from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key from cryptography.hazmat.backends import default_backend import base64 def rsa_encrypt(public_key_pem: str, plaintext: bytes) -> str: """使用 RSA 公钥加密数据""" public_key = load_pem_public_key(public_key_pem.encode(), backend=default_backend()) ciphertext = public_key.encrypt( plaintext, asym_padding.OAEP( mgf=asym_padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) return base64.b64encode(ciphertext).decode() def rsa_decrypt(private_key_pem: str, ciphertext_b64: str) -> bytes: """使用 RSA 私钥解密数据""" private_key = load_pem_private_key(private_key_pem.encode(), password=None, backend=default_backend()) ciphertext = base64.b64decode(ciphertext_b64) plaintext = private_key.decrypt( ciphertext, asym_padding.OAEP( mgf=asym_padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) return plaintext
注意:RSA 单次加密的长度受密钥长度限制(2048 位密钥最多加密 190 字节),因此我们只用 RSA 加密 AES 密钥,而不是直接加密业务数据。
三、AES-256-GCM 对称加密
AES-GCM 是业界推荐的加密模式——它同时提供加密和认证功能,能检测密文是否被篡改:
# crypto_aes.py — AES-256-GCM 加密/解密 from cryptography.hazmat.primitives.ciphers.aead import AESGCM import os, base64 def generate_aes_key() -> tuple: """生成 256 位 AES 密钥和 12 字节初始向量""" key = AESGCM.generate_key(bit_length=256) # 32 字节 nonce = os.urandom(12) # GCM 推荐 12 字节 IV return key, nonce def aes_encrypt(key: bytes, nonce: bytes, plaintext: str) -> str: """AES-256-GCM 加密""" aesgcm = AESGCM(key) ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None) # 返回 base64(nonce + ciphertext),其中包含认证标签(最后 16 字节) return base64.b64encode(nonce + ciphertext).decode() def aes_decrypt(key: bytes, encrypted_b64: str) -> str: """AES-256-GCM 解密(自带完整性校验)""" aesgcm = AESGCM(key) data = base64.b64decode(encrypted_b64) nonce, ciphertext = data[:12], data[12:] plaintext = aesgcm.decrypt(nonce, ciphertext, None) return plaintext.decode()
四、HMAC-SHA256 签名与验签
签名用于确保请求来自合法客户端且参数未被篡改。我们使用 HMAC-SHA256 对 timestamp + nonce + body 进行签名:
# crypto_sign.py — HMAC 签名与验证 import hmac, hashlib, time, os, json def build_sign(secret_key: str, method: str, path: str, timestamp: int, nonce: str, body: str) -> str: """构建签名串并按 HMAC-SHA256 签名""" sign_str = f"{method}\n{path}\n{timestamp}\n{nonce}\n{body}" signature = hmac.new( secret_key.encode(), sign_str.encode(), hashlib.sha256 ).hexdigest() return signature def verify_sign(secret_key: str, method: str, path: str, timestamp: int, nonce: str, body: str, signature: str) -> bool: """验证签名""" # 1. 检查时间戳是否在允许窗口内(5 分钟) now = int(time.time()) if abs(now - timestamp) > 300: return False # 请求已过期 # 2. 计算期望签名 expected = build_sign(secret_key, method, path, timestamp, nonce, body) # 3. 安全比较(防止时序攻击) return hmac.compare_digest(expected, signature)
签名传输方式:客户端将签名放在 HTTP 头 X-Signature 中,同时携带 X-Timestamp 和 X-Nonce 头。服务端优先从请求头读取签名字段。
五、防重放攻击:Nonce 机制
即使有签名,攻击者仍然可以截获一个合法请求并重复发送(重放攻击)。解决方案是 Nonce(一次性随机数)机制:
# nonce_store.py — 基于 Redis 的 Nonce 去重 import redis, time r = redis.Redis(host='localhost', port=6379, db=0) def check_nonce(nonce: str, timestamp: int, ttl: int = 300) -> bool: """ 检查 nonce 是否已被使用 返回 True 表示可用,False 表示重复(拒绝请求) """ key = f"nonce:{nonce}" # SETNX:仅在键不存在时设置,返回 1 成功 0 失败 result = r.setnx(key, str(timestamp)) if result == 0: return False # nonce 已存在 → 重放攻击! # 设置过期时间,防止 Redis 内存泄漏 r.expire(key, ttl) return True
Nonce 结合时间戳窗口(5 分钟内有效)构成双层防护:
- 时间戳窗口:淘汰过期请求
- Nonce 去重:保证同一请求只被处理一次
六、完整请求流程实战
下面是一个完整的支付下单请求的加密与签名流程:
# client.py — 支付客户端加密请求 import requests, json, time, os # 模拟订单数据 order_data = { "merchant_id": "M20260725001", "product_id": "card-qr-v3", "amount": "299.00", "currency": "CNY", "notify_url": "https://your-server.com/pay/notify", "timestamp": int(time.time()) } # 1. 生成 AES 密钥 aes_key, nonce = generate_aes_key() # 2. 用 AES 加密请求体 encrypted_body = aes_encrypt(aes_key, nonce, json.dumps(order_data)) # 3. 用 RSA 公钥加密 AES 密钥 with open("server_public_key.pem") as f: pub_key_pem = f.read() encrypted_key = rsa_encrypt(pub_key_pem, aes_key) # 4. 生成签名参数 t = int(time.time()) n = os.urandom(16).hex() sign = build_sign("your_api_secret", "POST", "/api/v1/pay/create", t, n, encrypted_body) # 5. 发送请求 payload = { "encrypted_key": encrypted_key, "encrypted_data": encrypted_body, "timestamp": t, "nonce": n, "sign": sign } resp = requests.post( "https://api.your-server.com/api/v1/pay/create", json=payload, headers={"Content-Type": "application/json"} ) print(f"Status: {resp.status_code}, Response: {resp.json()}")
# server.py — 服务端解密中间件(Flask 示例) from flask import Flask, request, jsonify, abort app = Flask(__name__) # 加载 RSA 私钥(从安全存储读取) with open("private_key.pem") as f: PRIVATE_KEY = f.read() API_SECRET = "your_api_secret" # 应与客户端一致 @app.route('/api/v1/pay/create', methods=['POST']) def pay_create(): data = request.get_json() # 1. 验签 & 防重放 if not verify_sign(API_SECRET, "POST", "/api/v1/pay/create", data['timestamp'], data['nonce'], data['encrypted_data'], data['sign']): return jsonify({"code": 401, "msg": "签名验证失败"}), 401 if not check_nonce(data['nonce'], data['timestamp']): return jsonify({"code": 401, "msg": "重复请求"}), 401 # 2. 用 RSA 私钥解密 AES 密钥 aes_key = rsa_decrypt(PRIVATE_KEY, data['encrypted_key']) # 3. 用 AES 密钥解密数据 order_json = aes_decrypt(aes_key, data['encrypted_data']) order = json.loads(order_json) # 4. 处理业务逻辑... print(f"收到订单: {order['merchant_id']} - ¥{order['amount']}") return jsonify({ "code": 0, "msg": "success", "data": {"order_no": "O" + str(int(time.time()))} })
七、签名算法的进阶优化
7.1 规范化的签名串
为了提高安全性,签名串应该对参数进行字典排序并去除空格,防止签名因参数顺序不同而验证失败:
# 规范化签名串生成 def build_canonical_sign(params: dict, secret_key: str) -> str: """ 按参数名升序拼接,去除值为空的参数 格式: k1=v1&k2=v2&key=SECRET """ sorted_keys = sorted(params.keys()) pairs = [] for k in sorted_keys: v = params[k] if v != '' and v is not None: pairs.append(f"{k}={v}") sign_str = '&'.join(pairs) sign_str += f"&key={secret_key}" return hashlib.md5(sign_str.encode()).hexdigest().upper()
这种规范化签名方式在微信支付 V2 API 和支付宝开放平台中被广泛使用,兼容性极好。
7.2 密钥轮换策略
定期更换密钥是支付安全的基本要求:
- RSA 密钥对建议每 90 天轮换一次
- AES 会话密钥每次请求都重新生成(一次性使用)
- HMAC 密钥建议每 30 天轮换
- 支持密钥版本号(key_version),在请求头中携带,服务端根据版本选择对应密钥验签
八、常见安全坑点与解决方案
| 坑点 | 风险 | 解决方案 |
|---|---|---|
| AES 密钥硬编码 | 密钥泄露 → 全量数据脱密 | 每次请求动态生成 RSA 加密传输 |
| 签名未包含请求体 | 中间人可替换 body 重放 | 签名必须包含 encrypted_data |
| Nonce 未设置过期 | Redis 内存无限增长 | 设置 TTL = 时间戳窗口值 (300s) |
| 时序攻击 | 通过比较时间推断签名 | 使用 hmac.compare_digest() 安全比较 |
| 未校验返回签名 | 伪造服务端响应 | 服务端也必须对返回数据签名 |
| 日志记录密文 | 敏感信息泄露 | 脱敏日志,仅记录 last 4 chars |
九、生产环境部署建议
在源码商城的转卡码系统中,我们采用了上述全套加密方案,并在生产环境运行数月无安全事件。以下是一些落地建议:
- 全链路加密:不只是下单接口,查询订单、退款、回调通知所有接口都要加密
- 密钥托管:使用 HashiCorp Vault 或 Amazon KMS 管理密钥,代码中不出现明文密钥
- 安全 SDK:将加密逻辑封装成统一 SDK(pip install pay-crypto),避免各业务线重复实现
- API 网关集成:在 Nginx/Kong 层面做签名验签,业务服务零改动
- 监控告警:签名失败次数超过阈值时自动告警,及时发现暴力破解尝试
# Nginx Lua 验签示例(在 API 网关层验签) location /api/ { access_by_lua_block { local timestamp = ngx.var.http_x_timestamp local nonce = ngx.var.http_x_nonce local sign = ngx.var.http_x_signature ngx.req.read_body() local body = ngx.req.get_body_data() -- 时间戳校验 if not timestamp or math.abs(ngx.time() - tonumber(timestamp)) > 300 then ngx.exit(401) end -- 签名校验(调用外部 HMAC 函数) local expected = hmac_sha256("secret", timestamp .. nonce .. body) if sign ~= expected then ngx.exit(401) end } proxy_pass http://backend; }
总结
支付系统的 API 安全不是一个可选项——它是关乎用户资金和企业生存的底线。本文从 RSA 非对称加密、AES 对称加密、HMAC 签名到 Nonce 防重放,完整讲解了支付 API 的安全通信体系。关键原则是:不要在 HTTPS 上裸奔应用层数据,加密和签名必须同时存在,时间戳和 Nonce 必须成对使用。
我们的源码商城转卡码系统(card-qr-v3)内置了完整的 API 加密框架,开箱即用。如果你在自建支付系统,以上代码可以直接复用到你的项目中。