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`。
|
- 泥点属于微信虚拟支付代币(coin),`short_series_coin` 的 `buyQuantity` 必须使用当前泥点商品的 `points_amount`;例如 60 泥点商品应传 `buyQuantity: 60`。
|
||||||
- 会员直购 `signData` 额外包含 `productId` 和 `goodsPrice`;`goodsPrice` 使用后端商品配置价,和微信后台道具价格校验保持一致。
|
- 会员直购 `signData` 额外包含 `productId` 和 `goodsPrice`;`goodsPrice` 使用后端商品配置价,和微信后台道具价格校验保持一致。
|
||||||
- 微信小程序“开发者服务器接收消息推送”必须配置为安全模式,数据格式选 JSON,URL 统一指向 `/api/profile/recharge/wechat/virtual-notify`。
|
- 微信小程序“开发者服务器接收消息推送”必须配置为安全模式,数据格式选 JSON,URL 统一指向 `/api/profile/recharge/wechat/virtual-notify`。
|
||||||
- `WECHAT_MINIPROGRAM_MESSAGE_TOKEN` 和 `WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY` 由环境变量注入;后端会先校验 `signature/msg_signature`,再用 `EncodingAESKey` 解密 `Encrypt`,然后按虚拟支付事件入账。
|
- `WECHAT_MINIPROGRAM_MESSAGE_TOKEN` 和 `WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY` 由环境变量注入;GET URL 验证按官方规则用 `Token/timestamp/nonce` 校验 `signature` 并原样返回 `echostr`,POST 安全模式推送再校验 `msg_signature`、用 `EncodingAESKey` 解密 `Encrypt`,然后按虚拟支付事件入账。
|
||||||
- 安全模式下,GET 验证会直接返回解密后的 `echostr`;POST 推送会先解密再解析 `xpay_goods_deliver_notify` / `xpay_coin_pay_notify`。
|
- 安全模式下,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,
|
Ok(value) => value,
|
||||||
Err(error) => return build_wechat_message_push_verify_error_response(error),
|
Err(error) => return build_wechat_message_push_verify_error_response(error),
|
||||||
};
|
};
|
||||||
let signature = query
|
match resolve_wechat_message_push_verify_response(
|
||||||
.msg_signature
|
token,
|
||||||
.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(
|
|
||||||
aes_key,
|
aes_key,
|
||||||
echostr,
|
|
||||||
state
|
state
|
||||||
.config
|
.config
|
||||||
.wechat_mini_program_app_id
|
.wechat_mini_program_app_id
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.or(state.config.wechat_app_id.as_deref()),
|
.or(state.config.wechat_app_id.as_deref()),
|
||||||
|
&query,
|
||||||
) {
|
) {
|
||||||
Ok(plaintext) => (StatusCode::OK, plaintext).into_response(),
|
Ok(plaintext) => (StatusCode::OK, plaintext).into_response(),
|
||||||
Err(error) => build_wechat_message_push_verify_error_response(error),
|
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()
|
(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(
|
fn parse_wechat_mini_program_message_push_payload(
|
||||||
body: &[u8],
|
body: &[u8],
|
||||||
) -> Result<WechatMiniProgramEncryptedMessage, WechatPayError> {
|
) -> 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]
|
#[test]
|
||||||
fn wechat_message_push_decrypts_safe_mode_ciphertext() {
|
fn wechat_message_push_decrypts_safe_mode_ciphertext() {
|
||||||
let app_id = "wx-test-app";
|
let app_id = "wx-test-app";
|
||||||
|
|||||||
Reference in New Issue
Block a user