admin/src/SimpleAdmin.Api/WeChat/WeChatMpCallbackSupport.cs
zzh 991ad49255 fix(微信): 加固 GET 验签,直接输出 echostr 原文
- 从 Query 读取 signature/timestamp/nonce/echostr 并记录失败原因

- 验签通过后 WriteAsync 写回 echostr,避免 JSON 包装

- 补充生产 Token 验签集成测试

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 13:43:42 +08:00

166 lines
5.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using SimpleAdmin.Api.WeChat.Options;
using SimpleAdmin.Api.WeChat.Services;
namespace SimpleAdmin.Api.WeChat;
internal static class WeChatMpCallbackSupport
{
/// <summary>
/// 微信 GET 接入验签:从 Query 读取参数,验签通过后原样输出 echostr。
/// </summary>
public static bool TryValidateGetRequest(
WeChatMpOptions options,
HttpRequest request,
ILogger? logger,
out string echostr)
{
echostr = string.Empty;
if (string.IsNullOrWhiteSpace(options.Token))
{
logger?.LogWarning("微信 GET 验签失败:未配置 Token");
return false;
}
var signature = request.Query["signature"].ToString();
var timestamp = request.Query["timestamp"].ToString();
var nonce = request.Query["nonce"].ToString();
echostr = request.Query["echostr"].ToString();
if (string.IsNullOrWhiteSpace(signature)
|| string.IsNullOrWhiteSpace(timestamp)
|| string.IsNullOrWhiteSpace(nonce)
|| string.IsNullOrEmpty(echostr))
{
logger?.LogWarning(
"微信 GET 验签失败:缺少必要参数 signature={HasSig} timestamp={HasTs} nonce={HasNonce} echostr={HasEcho}",
!string.IsNullOrWhiteSpace(signature),
!string.IsNullOrWhiteSpace(timestamp),
!string.IsNullOrWhiteSpace(nonce),
!string.IsNullOrEmpty(echostr));
return false;
}
if (!WeChatSignatureHelper.Validate(options.Token, timestamp, nonce, signature))
{
logger?.LogWarning(
"微信 GET 验签失败:signature 不匹配 timestamp={Timestamp} nonce={Nonce}",
timestamp,
nonce);
return false;
}
return true;
}
public static ContentResult Validate(
WeChatMpOptions options,
HttpRequest request,
string? signature,
string? timestamp,
string? nonce)
{
if (!TryValidateGetRequest(options, request, null, out var echostr))
{
return PlainError();
}
return EchoStr(echostr);
}
/// <summary>微信或探活可能发 HEAD,须返回 200(WMS/Spring 默认支持)。</summary>
public static ContentResult HeadOk() =>
new() { Content = string.Empty, ContentType = "text/plain; charset=utf-8", StatusCode = StatusCodes.Status200OK };
public static ContentResult PlainError() => PlainText("error");
public static async Task<ContentResult> HandleMessageAsync(
WeChatMpOptions options,
IWeChatEventHandler eventHandler,
HttpRequest request,
ILogger logger,
string signature,
string timestamp,
string nonce,
string? encryptType,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(options.Token))
{
logger.LogWarning("微信消息回调未配置 Token");
return PlainText("error");
}
// token、timestamp、nonce 字典序排序后拼接,SHA1 与 signature 对比
if (!WeChatSignatureHelper.Validate(options.Token, timestamp, nonce, signature))
{
logger.LogWarning("微信消息验签失败");
return PlainText("error");
}
using var reader = new StreamReader(request.Body);
var requestBody = await reader.ReadToEndAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(requestBody))
{
return AckSuccess();
}
// 明文模式:无 encrypt_type 或 encrypt_type=raw;安全模式 AES 暂不支持
if (!string.IsNullOrWhiteSpace(encryptType)
&& !string.Equals(encryptType, "raw", StringComparison.OrdinalIgnoreCase))
{
logger.LogWarning(
"收到加密微信消息({EncryptType}),当前仅支持明文模式,请在公众平台切换为明文或后续启用 AES。",
encryptType);
return AckSuccess();
}
var message = WeChatIncomingMessageParser.Parse(requestBody, request.ContentType);
if (message == null)
{
logger.LogWarning("无法解析微信消息体(支持明文 XML / JSON)");
return AckSuccess();
}
var reply = await eventHandler.HandleAsync(message, cancellationToken);
return BuildMessageReply(reply);
}
public static void EnsureAppId(WeChatMpOptions options, string appId)
{
var configured = options.AppId;
if (string.IsNullOrWhiteSpace(configured))
{
throw new BusinessException("未配置微信公众号 AppId");
}
if (!string.Equals(configured, appId, StringComparison.OrdinalIgnoreCase))
{
throw new BusinessException("公众号 AppId 不匹配");
}
}
private static ContentResult EchoStr(string echostr) =>
new() { Content = echostr, ContentType = "text/plain", StatusCode = StatusCodes.Status200OK };
private static ContentResult AckSuccess() => PlainText("success");
private static ContentResult BuildMessageReply(string? reply)
{
if (string.IsNullOrEmpty(reply))
{
return AckSuccess();
}
return reply.TrimStart().StartsWith('<') ? Xml(reply) : PlainText(reply);
}
private static ContentResult PlainText(string content) =>
new() { Content = content, ContentType = "text/plain; charset=utf-8", StatusCode = StatusCodes.Status200OK };
private static ContentResult Xml(string content) =>
new() { Content = content, ContentType = "application/xml; charset=utf-8", StatusCode = StatusCodes.Status200OK };
}