diff --git a/src/SimpleAdmin.Api.IntegrationTests/WeChatRouteIntegrationTests.cs b/src/SimpleAdmin.Api.IntegrationTests/WeChatRouteIntegrationTests.cs index 9f3a290..25dc374 100644 --- a/src/SimpleAdmin.Api.IntegrationTests/WeChatRouteIntegrationTests.cs +++ b/src/SimpleAdmin.Api.IntegrationTests/WeChatRouteIntegrationTests.cs @@ -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(options => options.Token = token); + }); + + var response = await host.Client.GetAsync( + $"/apiwechat/verification?signature={signature}×tamp={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(options => diff --git a/src/SimpleAdmin.Api/Auth/AuthService.WeChatMp.cs b/src/SimpleAdmin.Api/Auth/AuthService.WeChatMp.cs index df386f8..623337b 100644 --- a/src/SimpleAdmin.Api/Auth/AuthService.WeChatMp.cs +++ b/src/SimpleAdmin.Api/Auth/AuthService.WeChatMp.cs @@ -29,21 +29,18 @@ public partial class AuthService [HttpGet("/apiwechat/verification")] [Produces("text/plain")] [ApiExplorerSettings(GroupName = "WeChat")] - public Task WeChatValidateLegacyAsync( + public Task 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 WeChatValidateCoreAsync( - string? signature, - string? timestamp, - string? nonce, - CancellationToken cancellationToken) + private async Task 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 WeChatValidateAsync( + public async Task 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(); } /// diff --git a/src/SimpleAdmin.Api/WeChat/WeChatMpCallbackSupport.cs b/src/SimpleAdmin.Api/WeChat/WeChatMpCallbackSupport.cs index da76038..908dedd 100644 --- a/src/SimpleAdmin.Api/WeChat/WeChatMpCallbackSupport.cs +++ b/src/SimpleAdmin.Api/WeChat/WeChatMpCallbackSupport.cs @@ -7,6 +7,54 @@ namespace SimpleAdmin.Api.WeChat; internal static class WeChatMpCallbackSupport { + /// + /// 微信 GET 接入验签:从 Query 读取参数,验签通过后原样输出 echostr。 + /// + 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); } /// 微信或探活可能发 HEAD,须返回 200(WMS/Spring 默认支持)。