feat: 部署时自动校验支付宝签名一致性 + 验签失败升级为 error + 密钥核对脚本
This commit is contained in:
80
lib/test.sh
80
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[@]}
|
||||
|
||||
|
||||
65
scripts/verify-alipay-keys.sh
Executable file
65
scripts/verify-alipay-keys.sh
Executable 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 "请到支付宝开放平台 → 密钥工具 → 查看应用公钥,与上方公钥比对"
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user