fix: 支付宝签名使用原始值,改用表单自动提交方式
- 修复签名源:使用原始参数值(不进行 URL 编码) - 改用 HTML 表单自动提交方式代替服务器端 API 调用 - 避免 nginx 代理大响应体的问题
This commit is contained in:
@@ -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 ¶ms {
|
||||||
.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,
|
||||||
¬ify_url,
|
¬ify_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,
|
||||||
¬ify_url,
|
¬ify_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)),
|
||||||
|
|||||||
Reference in New Issue
Block a user