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)
|
||||
}
|
||||
|
||||
// ===== 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 ¶ms {
|
||||
form_fields.push_str(&format!(
|
||||
r#"<input type="hidden" name="{}" value="{}" />"#,
|
||||
urlencoding(k),
|
||||
urlencoding(v)
|
||||
));
|
||||
}
|
||||
|
||||
let json_str = parts[0]
|
||||
.strip_prefix("alipay_trade_page_pay_response=")
|
||||
.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
|
||||
form_fields.push_str(&format!(
|
||||
r#"<input type="hidden" name="sign" value="{}" />"#,
|
||||
urlencoding(&sign)
|
||||
));
|
||||
}
|
||||
|
||||
let form_html = json
|
||||
.get("form_html")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| "响应中缺少 form_html".to_string())?;
|
||||
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
|
||||
);
|
||||
|
||||
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,
|
||||
¬ify_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,
|
||||
¬ify_url,
|
||||
&return_url,
|
||||
)
|
||||
.await
|
||||
{
|
||||
) {
|
||||
Ok(form_html) => Ok(HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(form_html)),
|
||||
|
||||
Reference in New Issue
Block a user