fix: 支付宝签名使用原始值,改用表单自动提交方式

- 修复签名源:使用原始参数值(不进行 URL 编码)
- 改用 HTML 表单自动提交方式代替服务器端 API 调用
- 避免 nginx 代理大响应体的问题
This commit is contained in:
2026-05-07 15:19:34 +08:00
parent 4aece40a58
commit 7a194251be

View File

@@ -128,9 +128,9 @@ fn rsa2_verify(content: &str, sign: &str, public_key_pem: &str) -> Result<bool,
Ok(true)
}
// ===== Helper: 调用 alipay.trade.page.pay =====
// ===== Helper: 生成支付宝支付表单 HTML =====
async fn call_alipay_trade_page_pay(
fn build_alipay_form_html(
config: &AlipayConfig,
out_trade_no: &str,
total_amount: &str,
@@ -138,11 +138,6 @@ async fn call_alipay_trade_page_pay(
notify_url: &str,
return_url: &str,
) -> Result<String, String> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| format!("HTTP 客户端创建失败: {}", e))?;
let biz_content = serde_json::json!({
"out_trade_no": out_trade_no,
"total_amount": total_amount,
@@ -166,79 +161,44 @@ async fn call_alipay_trade_page_pay(
params.insert("notify_url", notify_url);
params.insert("return_url", return_url);
// 构造待签名串
// 构造待签名串(注意:签名源使用原始值,不做 URL 编码)
let sign_source: String = params
.iter()
.map(|(k, v)| format!("{}={}", k, urlencoding(v)))
.map(|(k, v)| format!("{}={}", k, *v))
.collect::<Vec<_>>()
.join("&");
let sign = rsa2_sign(&sign_source, &config.private_key)?;
// 构建 POST body
let query: String = params
.iter()
.map(|(k, v)| format!("{}={}", urlencoding(k), urlencoding(v)))
.chain(std::iter::once(format!("sign={}", urlencoding(&sign))))
.collect::<Vec<_>>()
.join("&");
let resp = client
.post(&config.gateway)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(query)
.send()
.await
.map_err(|e| format!("请求支付宝失败: {}", e))?;
let body = resp.text().await.map_err(|e| format!("读取响应失败: {}", e))?;
// 支付宝返回格式: alipay_trade_page_pay_response={...}&sign=xxx
let parts: Vec<&str> = body.splitn(2, "&sign=").collect();
if parts.len() != 2 {
// 如果直接返回 HTML 表单(沙箱环境可能直接返回表单),直接返回
if body.contains("<form") {
return Ok(body);
}
return Err(format!(
"支付宝响应格式异常: {}",
&body[..body.len().min(300)]
// 生成自动提交的表单 HTML
let mut form_fields = String::new();
for (k, v) in &params {
form_fields.push_str(&format!(
r#"<input type="hidden" name="{}" value="{}" />"#,
urlencoding(k),
urlencoding(v)
));
}
form_fields.push_str(&format!(
r#"<input type="hidden" name="sign" value="{}" />"#,
urlencoding(&sign)
));
let json_str = parts[0]
.strip_prefix("alipay_trade_page_pay_response=")
.unwrap_or(parts[0]);
let html = format!(
r##"<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>正在跳转支付宝...</title></head>
<body>
<form id="alipay" action="{}" method="post">
{}
</form>
<script>document.getElementById('alipay').submit();</script>
</body>
</html>"##,
config.gateway, form_fields
);
let sign_from_alipay = parts[1];
if !rsa2_verify(json_str, sign_from_alipay, &config.alipay_public_key)? {
return Err("支付宝响应验签失败".to_string());
}
let json: serde_json::Value =
serde_json::from_str(json_str).map_err(|e| format!("JSON 解析失败: {}", e))?;
if json.get("code").and_then(|v| v.as_str()) != Some("10000") {
let sub_msg = json
.get("sub_msg")
.and_then(|v| v.as_str())
.unwrap_or("");
return Err(format!(
"支付宝接口错误: {} - {}",
json.get("msg")
.and_then(|v| v.as_str())
.unwrap_or("未知"),
sub_msg
));
}
let form_html = json
.get("form_html")
.and_then(|v| v.as_str())
.ok_or_else(|| "响应中缺少 form_html".to_string())?;
Ok(form_html.to_string())
Ok(html)
}
// ===== 提取 JWT token =====
@@ -663,16 +623,14 @@ function confirmMockPay() {{
let total_amount_str = format!("{:.2}", pkg.amount as f64 / 100.0);
match call_alipay_trade_page_pay(
match build_alipay_form_html(
&config,
&order_no,
&total_amount_str,
pkg.display_name,
&notify_url,
&return_url,
)
.await
{
) {
Ok(form_html) => Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(form_html)),
@@ -764,16 +722,14 @@ function confirmMockPay() {{
let total_amount_str = format!("{:.2}", pkg.amount as f64 / 100.0);
match call_alipay_trade_page_pay(
match build_alipay_form_html(
&config,
&query.order_no,
&total_amount_str,
pkg.display_name,
&notify_url,
&return_url,
)
.await
{
) {
Ok(form_html) => Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(form_html)),