feat: 部署时自动校验支付宝签名一致性 + 验签失败升级为 error + 密钥核对脚本

This commit is contained in:
2026-08-10 16:06:02 +08:00
parent 49d740e27c
commit 366d62f262
3 changed files with 147 additions and 2 deletions

View File

@@ -499,6 +499,85 @@ test_notify_url_domain() {
return 0 return 0
} }
# ---------- 支付宝签名一致性校验 ----------
# 防止密钥不同步导致的 invalid-signature历史事故服务器私钥与控制台应用公钥不匹配
test_payment_signature() {
log_info "校验支付宝支付签名与本地密钥一致性..."
if [ ! -f "${SCRIPT_DIR}/.env" ] || ! grep -q "ALIPAY_PRIVATE_KEY" "${SCRIPT_DIR}/.env"; then
log_warn " 本地 .env 无 ALIPAY_PRIVATE_KEY跳过签名校验"
return 0
fi
local jwt
jwt=$(remote "curl -sk -X POST '${BASE_URL}/api/guest-login'" 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
if [ -z "$jwt" ]; then
log_error "获取 JWT 失败,无法校验支付签名"
return 1
fi
local form
form=$(remote "curl -sk --max-time 10 '${BASE_URL}/payment/page?package=monthly&jwt=${jwt}'" 2>/dev/null || true)
if ! echo "$form" | grep -q 'name="sign"'; then
log_warn " 支付表单未生成(可能为 Mock 支付模式),跳过签名校验"
return 0
fi
local result
result=$(echo "$form" | python3 -c "
import base64, re, html, sys
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
priv_b64 = None
for line in open('${SCRIPT_DIR}/.env'):
if line.startswith('ALIPAY_PRIVATE_KEY='):
priv_b64 = line.strip().split('=', 1)[1]
break
if not priv_b64:
print('FAIL: 本地 .env 无 ALIPAY_PRIVATE_KEY')
sys.exit(1)
try:
priv = serialization.load_der_private_key(base64.b64decode(priv_b64), password=None)
except Exception:
try:
priv = serialization.load_pem_private_key(priv_b64.encode(), password=None)
except Exception as e:
print(f'FAIL: 私钥解析失败: {e}')
sys.exit(1)
content = sys.stdin.read()
fields = {}
for m in re.finditer(r'name=\"([^\"]+)\" value=\"([^\"]*)\"', content):
fields[m.group(1)] = html.unescape(m.group(2))
if 'sign' not in fields:
print('FAIL: 表单无 sign 字段')
sys.exit(1)
sign = fields.pop('sign')
sign_source = '&'.join(f'{k}={v}' for k, v in sorted(fields.items()))
try:
our_sign = base64.b64encode(priv.sign(sign_source.encode('utf-8'), padding.PKCS1v15(), hashes.SHA256())).decode()
except Exception as e:
print(f'FAIL: 签名失败: {e}')
sys.exit(1)
if our_sign == sign:
print('PASS')
else:
print('FAIL: 服务器签名与本地私钥不一致(密钥不同步!检查服务器 .env 与控制台应用公钥)')
sys.exit(1)
" 2>/dev/null)
if [ "$result" = "PASS" ]; then
log_info " 服务器签名与本地私钥一致 ✅"
return 0
else
log_error " ${result:-签名校验脚本执行失败}"
return 1
fi
}
# ---------- 主测试入口 ---------- # ---------- 主测试入口 ----------
run_tests() { run_tests() {
log_step "执行部署后测试..." log_step "执行部署后测试..."
@@ -522,6 +601,7 @@ run_tests() {
"test_payment_refund:退款+会员撤销" "test_payment_refund:退款+会员撤销"
"test_payment_verify_detection:验证函数检测逻辑" "test_payment_verify_detection:验证函数检测逻辑"
"test_notify_url_domain:支付回调域名校验" "test_notify_url_domain:支付回调域名校验"
"test_payment_signature:支付签名一致性"
) )
local i=1 total=${#tests[@]} local i=1 total=${#tests[@]}

65
scripts/verify-alipay-keys.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# ===========================================
# 支付宝密钥一致性核对脚本
# 对比本地 .env 与服务器 green/blue .env 的应用私钥,
# 并从本地私钥导出应用公钥,供与支付宝控制台比对。
# 防止 invalid-signature密钥不同步问题复发。
# 用法: bash scripts/verify-alipay-keys.sh [--remote-host HOST]
# ===========================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
ENV_FILE="$PROJECT_ROOT/.env"
REMOTE_HOST="${2:-aliyun-server}"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
if [ ! -f "$ENV_FILE" ] || ! grep -q '^ALIPAY_PRIVATE_KEY=' "$ENV_FILE"; then
error "本地 ${ENV_FILE} 未找到 ALIPAY_PRIVATE_KEY"
exit 1
fi
LOCAL_KEY=$(grep '^ALIPAY_PRIVATE_KEY=' "$ENV_FILE" | head -1 | cut -d'=' -f2- | tr -d '\r\n ')
LOCAL_MD5=$(printf '%s' "$LOCAL_KEY" | md5sum | cut -d' ' -f1)
info "本地 .env 私钥 MD5: ${LOCAL_MD5}"
ALL_MATCH=true
for ENV in green blue; do
REMOTE_MD5=$(ssh "$REMOTE_HOST" "grep '^ALIPAY_PRIVATE_KEY=' /root/rust/rust_backend_${ENV}/.env | cut -c20- | tr -d '\n' | md5sum | cut -d' ' -f1" 2>/dev/null || echo "N/A")
if [ "$REMOTE_MD5" = "$LOCAL_MD5" ]; then
info "服务器 ${ENV} 私钥一致 ✅"
else
warn "服务器 ${ENV} 私钥不一致(本地=${LOCAL_MD5} 远端=${REMOTE_MD5}"
ALL_MATCH=false
fi
done
if [ "$ALL_MATCH" = true ]; then
info "本地与服务器密钥一致"
else
warn "存在不一致!请检查服务器 .env 或重新部署"
fi
echo ""
echo "========== 应用公钥(与控制台比对) =========="
python3 -c "
import base64
from cryptography.hazmat.primitives import serialization
key_b64 = '$LOCAL_KEY'
try:
priv = serialization.load_der_private_key(base64.b64decode(key_b64), password=None)
except Exception:
priv = serialization.load_pem_private_key(key_b64.encode(), password=None)
pub_der = priv.public_key().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
print(base64.b64encode(pub_der).decode())
"
echo ""
info "请到支付宝开放平台 → 密钥工具 → 查看应用公钥,与上方公钥比对"

View File

@@ -1027,7 +1027,7 @@ pub async fn alipay_notify(
.join("&"); .join("&");
if let Err(e) = rsa2_verify(&sign_source, &sign, &config.alipay_public_key) { if let Err(e) = rsa2_verify(&sign_source, &sign, &config.alipay_public_key) {
tracing::warn!("支付宝签名验证失败: {}", e); tracing::error!("支付宝回调签名验证失败: {}", e);
return HttpResponse::Ok().body("fail"); return HttpResponse::Ok().body("fail");
} }
@@ -1213,7 +1213,7 @@ pub async fn alipay_refund_notify(
.join("&"); .join("&");
if let Err(e) = rsa2_verify(&sign_source, &sign, &config.alipay_public_key) { if let Err(e) = rsa2_verify(&sign_source, &sign, &config.alipay_public_key) {
tracing::warn!("支付宝退款签名验证失败: {}", e); tracing::error!("支付宝退款回调签名验证失败: {}", e);
return HttpResponse::Ok().body("fail"); return HttpResponse::Ok().body("fail");
} }