diff --git a/lib/test.sh b/lib/test.sh index b38297f..26bc62b 100644 --- a/lib/test.sh +++ b/lib/test.sh @@ -499,6 +499,85 @@ test_notify_url_domain() { 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() { log_step "执行部署后测试..." @@ -522,6 +601,7 @@ run_tests() { "test_payment_refund:退款+会员撤销" "test_payment_verify_detection:验证函数检测逻辑" "test_notify_url_domain:支付回调域名校验" + "test_payment_signature:支付签名一致性" ) local i=1 total=${#tests[@]} diff --git a/scripts/verify-alipay-keys.sh b/scripts/verify-alipay-keys.sh new file mode 100755 index 0000000..cbdc371 --- /dev/null +++ b/scripts/verify-alipay-keys.sh @@ -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 "请到支付宝开放平台 → 密钥工具 → 查看应用公钥,与上方公钥比对" diff --git a/src/handlers/payment.rs b/src/handlers/payment.rs index 1153ddf..7f171b9 100644 --- a/src/handlers/payment.rs +++ b/src/handlers/payment.rs @@ -1027,7 +1027,7 @@ pub async fn alipay_notify( .join("&"); if let Err(e) = rsa2_verify(&sign_source, &sign, &config.alipay_public_key) { - tracing::warn!("支付宝签名验证失败: {}", e); + tracing::error!("支付宝回调签名验证失败: {}", e); return HttpResponse::Ok().body("fail"); } @@ -1213,7 +1213,7 @@ pub async fn alipay_refund_notify( .join("&"); if let Err(e) = rsa2_verify(&sign_source, &sign, &config.alipay_public_key) { - tracing::warn!("支付宝退款签名验证失败: {}", e); + tracing::error!("支付宝退款回调签名验证失败: {}", e); return HttpResponse::Ok().body("fail"); }