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[@]}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user