fix: handle wechat message url verification

This commit is contained in:
kdletters
2026-06-01 22:52:51 +08:00
parent fc4b04a812
commit fae4db6a09
2 changed files with 74 additions and 25 deletions

View File

@@ -43,8 +43,8 @@ WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV=0
- 泥点属于微信虚拟支付代币coin`short_series_coin``buyQuantity` 必须使用当前泥点商品的 `points_amount`;例如 60 泥点商品应传 `buyQuantity: 60`
- 会员直购 `signData` 额外包含 `productId``goodsPrice``goodsPrice` 使用后端商品配置价,和微信后台道具价格校验保持一致。
- 微信小程序“开发者服务器接收消息推送”必须配置为安全模式,数据格式选 JSONURL 统一指向 `/api/profile/recharge/wechat/virtual-notify`
- `WECHAT_MINIPROGRAM_MESSAGE_TOKEN``WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY` 由环境变量注入;后端会先校验 `signature/msg_signature`,再`EncodingAESKey` 解密 `Encrypt`,然后按虚拟支付事件入账。
- 安全模式下,GET 验证会直接返回解密后的 `echostr`POST 推送会先解密再解析 `xpay_goods_deliver_notify` / `xpay_coin_pay_notify`
- `WECHAT_MINIPROGRAM_MESSAGE_TOKEN``WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY` 由环境变量注入;GET URL 验证按官方规则用 `Token/timestamp/nonce` 校验 `signature` 并原样返回 `echostr`POST 安全模式推送再校验 `msg_signature``EncodingAESKey` 解密 `Encrypt`,然后按虚拟支付事件入账。
- 安全模式下POST 推送会先解密再解析 `xpay_goods_deliver_notify` / `xpay_coin_pay_notify`;不要把 GET URL 验证里的 `echostr` 当密文解密
## 验收命令

View File

@@ -901,35 +901,15 @@ pub async fn handle_wechat_virtual_payment_message_push_verify(
Ok(value) => value,
Err(error) => return build_wechat_message_push_verify_error_response(error),
};
let signature = query
.msg_signature
.as_deref()
.or(query.signature.as_deref())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("");
let timestamp = query.timestamp.as_deref().map(str::trim).unwrap_or("");
let nonce = query.nonce.as_deref().map(str::trim).unwrap_or("");
let echostr = query.echostr.as_deref().map(str::trim).unwrap_or("");
if signature.is_empty() || timestamp.is_empty() || nonce.is_empty() || echostr.is_empty() {
return build_wechat_message_push_verify_error_response(WechatPayError::InvalidRequest(
"微信消息推送校验参数不完整".to_string(),
));
}
if !verify_wechat_message_push_signature(token, timestamp, nonce, echostr, signature) {
return build_wechat_message_push_verify_error_response(WechatPayError::InvalidSignature(
"微信消息推送校验签名无效".to_string(),
));
}
match decrypt_wechat_message_push_ciphertext(
match resolve_wechat_message_push_verify_response(
token,
aes_key,
echostr,
state
.config
.wechat_mini_program_app_id
.as_deref()
.or(state.config.wechat_app_id.as_deref()),
&query,
) {
Ok(plaintext) => (StatusCode::OK, plaintext).into_response(),
Err(error) => build_wechat_message_push_verify_error_response(error),
@@ -1139,6 +1119,50 @@ fn build_wechat_message_push_verify_error_response(error: WechatPayError) -> Res
(StatusCode::BAD_REQUEST, message).into_response()
}
fn resolve_wechat_message_push_verify_response(
token: &str,
aes_key: &str,
expected_app_id: Option<&str>,
query: &WechatMiniProgramMessagePushQuery,
) -> Result<String, WechatPayError> {
let timestamp = query.timestamp.as_deref().map(str::trim).unwrap_or("");
let nonce = query.nonce.as_deref().map(str::trim).unwrap_or("");
let echostr = query.echostr.as_deref().map(str::trim).unwrap_or("");
if timestamp.is_empty() || nonce.is_empty() || echostr.is_empty() {
return Err(WechatPayError::InvalidRequest(
"微信消息推送校验参数不完整".to_string(),
));
}
let msg_signature = query
.msg_signature
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
if let Some(signature) = msg_signature {
if !verify_wechat_message_push_signature(token, timestamp, nonce, echostr, signature) {
return Err(WechatPayError::InvalidSignature(
"微信消息推送 msg_signature 无效".to_string(),
));
}
return decrypt_wechat_message_push_ciphertext(aes_key, echostr, expected_app_id);
}
let signature = query
.signature
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
WechatPayError::InvalidRequest("微信消息推送校验参数不完整".to_string())
})?;
if !verify_wechat_message_push_signature(token, timestamp, nonce, "", signature) {
return Err(WechatPayError::InvalidSignature(
"微信消息推送校验签名无效".to_string(),
));
}
Ok(echostr.to_string())
}
fn parse_wechat_mini_program_message_push_payload(
body: &[u8],
) -> Result<WechatMiniProgramEncryptedMessage, WechatPayError> {
@@ -2217,6 +2241,31 @@ mod tests {
));
}
#[test]
fn wechat_message_push_plain_get_verify_returns_echostr() {
let token = "AAAAA";
let timestamp = "1714036504";
let nonce = "1514711492";
let echostr = "4375120948345356249";
let signature = "f464b24fc39322e44b38aa78f5edd27bd1441696";
let plaintext = resolve_wechat_message_push_verify_response(
token,
"unused-aes-key",
Some("wx-test-app"),
&WechatMiniProgramMessagePushQuery {
signature: Some(signature.to_string()),
timestamp: Some(timestamp.to_string()),
nonce: Some(nonce.to_string()),
echostr: Some(echostr.to_string()),
msg_signature: None,
},
)
.expect("plain url verification should return echostr");
assert_eq!(plaintext, echostr);
}
#[test]
fn wechat_message_push_decrypts_safe_mode_ciphertext() {
let app_id = "wx-test-app";