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) Ok(true)
} }
// ===== Helper: 调用 alipay.trade.page.pay ===== // ===== Helper: 生成支付宝支付表单 HTML =====
async fn call_alipay_trade_page_pay( fn build_alipay_form_html(
config: &AlipayConfig, config: &AlipayConfig,
out_trade_no: &str, out_trade_no: &str,
total_amount: &str, total_amount: &str,
@@ -138,11 +138,6 @@ async fn call_alipay_trade_page_pay(
notify_url: &str, notify_url: &str,
return_url: &str, return_url: &str,
) -> Result<String, String> { ) -> 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!({ let biz_content = serde_json::json!({
"out_trade_no": out_trade_no, "out_trade_no": out_trade_no,
"total_amount": total_amount, "total_amount": total_amount,
@@ -166,79 +161,44 @@ async fn call_alipay_trade_page_pay(
params.insert("notify_url", notify_url); params.insert("notify_url", notify_url);
params.insert("return_url", return_url); params.insert("return_url", return_url);
// 构造待签名串 // 构造待签名串(注意:签名源使用原始值,不做 URL 编码)
let sign_source: String = params let sign_source: String = params
.iter() .iter()
.map(|(k, v)| format!("{}={}", k, urlencoding(v))) .map(|(k, v)| format!("{}={}", k, *v))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("&"); .join("&");
let sign = rsa2_sign(&sign_source, &config.private_key)?; let sign = rsa2_sign(&sign_source, &config.private_key)?;
// 构建 POST body // 生成自动提交的表单 HTML
let query: String = params let mut form_fields = String::new();
.iter() for (k, v) in &params {
.map(|(k, v)| format!("{}={}", urlencoding(k), urlencoding(v))) form_fields.push_str(&format!(
.chain(std::iter::once(format!("sign={}", urlencoding(&sign)))) r#"<input type="hidden" name="{}" value="{}" />"#,
.collect::<Vec<_>>() urlencoding(k),
.join("&"); urlencoding(v)
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)]
)); ));
} }
form_fields.push_str(&format!(
let json_str = parts[0] r#"<input type="hidden" name="sign" value="{}" />"#,
.strip_prefix("alipay_trade_page_pay_response=") urlencoding(&sign)
.unwrap_or(parts[0]);
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 let html = format!(
.get("form_html") r##"<!DOCTYPE html>
.and_then(|v| v.as_str()) <html>
.ok_or_else(|| "响应中缺少 form_html".to_string())?; <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
);
Ok(form_html.to_string()) Ok(html)
} }
// ===== 提取 JWT token ===== // ===== 提取 JWT token =====
@@ -663,16 +623,14 @@ function confirmMockPay() {{
let total_amount_str = format!("{:.2}", pkg.amount as f64 / 100.0); let total_amount_str = format!("{:.2}", pkg.amount as f64 / 100.0);
match call_alipay_trade_page_pay( match build_alipay_form_html(
&config, &config,
&order_no, &order_no,
&total_amount_str, &total_amount_str,
pkg.display_name, pkg.display_name,
&notify_url, &notify_url,
&return_url, &return_url,
) ) {
.await
{
Ok(form_html) => Ok(HttpResponse::Ok() Ok(form_html) => Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8") .content_type("text/html; charset=utf-8")
.body(form_html)), .body(form_html)),
@@ -764,16 +722,14 @@ function confirmMockPay() {{
let total_amount_str = format!("{:.2}", pkg.amount as f64 / 100.0); let total_amount_str = format!("{:.2}", pkg.amount as f64 / 100.0);
match call_alipay_trade_page_pay( match build_alipay_form_html(
&config, &config,
&query.order_no, &query.order_no,
&total_amount_str, &total_amount_str,
pkg.display_name, pkg.display_name,
&notify_url, &notify_url,
&return_url, &return_url,
) ) {
.await
{
Ok(form_html) => Ok(HttpResponse::Ok() Ok(form_html) => Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8") .content_type("text/html; charset=utf-8")
.body(form_html)), .body(form_html)),