fix: handle wechat message url verification
This commit is contained in:
@@ -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` 使用后端商品配置价,和微信后台道具价格校验保持一致。
|
||||
- 微信小程序“开发者服务器接收消息推送”必须配置为安全模式,数据格式选 JSON,URL 统一指向 `/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` 当密文解密。
|
||||
|
||||
## 验收命令
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user