fix(微信): 加固 GET 验签,直接输出 echostr 原文

- 从 Query 读取 signature/timestamp/nonce/echostr 并记录失败原因

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-04 13:43:42 +08:00
parent da1227d8aa
commit 991ad49255
3 changed files with 103 additions and 30 deletions

View File

@ -83,6 +83,29 @@ public class WeChatRouteIntegrationTests
body.Should().NotStartWith("{");
}
[Fact]
public async Task WeChatValidateLegacy_WithProductionToken_ShouldReturnEchostr()
{
const string token = "c6ca74013df823c0e1da0feaaaf8bf66";
const string timestamp = "1714037059";
const string nonce = "486452656";
const string echostr = "5938472619053847261";
var signature = SignWeChat(token, timestamp, nonce);
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
{
ConfigureWeChatFallback(services);
services.PostConfigure<WeChatMpOptions>(options => options.Token = token);
});
var response = await host.Client.GetAsync(
$"/apiwechat/verification?signature={signature}&timestamp={timestamp}&nonce={nonce}&echostr={echostr}");
response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Content.Headers.ContentType?.MediaType.Should().Be("text/plain");
(await response.Content.ReadAsStringAsync()).Should().Be(echostr);
}
private static void ConfigureWeChatFallback(IServiceCollection services)
{
services.PostConfigure<WeChatMpOptions>(options =>

View File

@ -29,21 +29,18 @@ public partial class AuthService
[HttpGet("/apiwechat/verification")]
[Produces("text/plain")]
[ApiExplorerSettings(GroupName = "WeChat")]
public Task<ContentResult> WeChatValidateLegacyAsync(
public Task<IActionResult> WeChatValidateLegacyAsync(
[FromQuery] string? signature,
[FromQuery] string? timestamp,
[FromQuery] string? nonce,
[FromQuery] string? echostr,
CancellationToken cancellationToken = default) =>
WeChatValidateCoreAsync(signature, timestamp, nonce, cancellationToken);
WeChatValidateCoreAsync(cancellationToken);
private async Task<ContentResult> WeChatValidateCoreAsync(
string? signature,
string? timestamp,
string? nonce,
CancellationToken cancellationToken)
private async Task<IActionResult> WeChatValidateCoreAsync(CancellationToken cancellationToken)
{
var request = httpContextAccessor.HttpContext?.Request;
var httpContext = httpContextAccessor.HttpContext;
var request = httpContext?.Request;
if (request == null)
{
return WeChatMpCallbackSupport.PlainError();
@ -52,7 +49,17 @@ public partial class AuthService
try
{
var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
return WeChatMpCallbackSupport.Validate(options, request, signature, timestamp, nonce);
if (!WeChatMpCallbackSupport.TryValidateGetRequest(options, request, WeChatLogger, out var echo))
{
return WeChatMpCallbackSupport.PlainError();
}
// 微信要求:HTTP 200 + 响应体仅为 echostr 原文,不可 JSON 包装
var response = httpContext!.Response;
response.StatusCode = StatusCodes.Status200OK;
response.ContentType = "text/plain";
await response.WriteAsync(echo, cancellationToken);
return new EmptyResult();
}
catch (Exception ex)
{
@ -68,7 +75,7 @@ public partial class AuthService
[HttpGet("/wechat/mp/{appId}")]
[Produces("text/plain")]
[ApiExplorerSettings(GroupName = "WeChat")]
public async Task<ContentResult> WeChatValidateAsync(
public async Task<IActionResult> WeChatValidateAsync(
[FromRoute] string appId,
[FromQuery] string? signature,
[FromQuery] string? timestamp,
@ -76,11 +83,21 @@ public partial class AuthService
[FromQuery] string? echostr,
CancellationToken cancellationToken = default)
{
var request = httpContextAccessor.HttpContext?.Request
?? throw new BusinessException("无法读取微信验签请求");
var httpContext = httpContextAccessor.HttpContext
?? throw new BusinessException("无法读取微信验签请求");
var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
WeChatMpCallbackSupport.EnsureAppId(options, appId);
return WeChatMpCallbackSupport.Validate(options, request, signature, timestamp, nonce);
if (!WeChatMpCallbackSupport.TryValidateGetRequest(options, httpContext.Request, WeChatLogger, out var echo))
{
return WeChatMpCallbackSupport.PlainError();
}
var response = httpContext.Response;
response.StatusCode = StatusCodes.Status200OK;
response.ContentType = "text/plain";
await response.WriteAsync(echo, cancellationToken);
return new EmptyResult();
}
/// <summary>

View File

@ -7,6 +7,54 @@ 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,
@ -14,27 +62,12 @@ internal static class WeChatMpCallbackSupport
string? timestamp,
string? nonce)
{
// 微信服务器要求 HTTP 200 + 纯文本 echostr,不可抛 BusinessException(会变成 JSON 非 200)
if (string.IsNullOrWhiteSpace(options.Token))
if (!TryValidateGetRequest(options, request, null, out var echostr))
{
return PlainError();
}
var echostr = request.Query.TryGetValue("echostr", out var echostrValue)
? echostrValue.ToString()
: string.Empty;
if (string.IsNullOrWhiteSpace(signature)
|| string.IsNullOrWhiteSpace(timestamp)
|| string.IsNullOrWhiteSpace(nonce)
|| string.IsNullOrEmpty(echostr))
{
return PlainError();
}
var valid = WeChatSignatureHelper.Validate(options.Token, timestamp, nonce, signature);
// 验签通过后原样返回 echostr,不可包 JSON、不可加引号或换行
return valid ? EchoStr(echostr) : PlainError();
return EchoStr(echostr);
}
/// <summary>微信或探活可能发 HEAD,须返回 200(WMS/Spring 默认支持)。</summary>