fix(微信): 验签接口仅保留 GET 并原样返回 echostr

- /apiwechat/verification 移除 HEAD/POST,仅保留 GET 验签

- 验签通过后从 Query 原样返回 echostr 纯文本

- 扫码回调测试改走 /wechat/mp/{appId}

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-04 12:30:42 +08:00
parent 0a5bd75618
commit 6c2e095196
4 changed files with 47 additions and 61 deletions

View File

@ -16,13 +16,12 @@ namespace SimpleAdmin.Api.IntegrationTests;
public class WeChatRouteIntegrationTests public class WeChatRouteIntegrationTests
{ {
[Fact] [Fact]
public async Task WeChatLegacyVerification_ShouldRegisterGetAndPostMethods() public async Task WeChatLegacyVerification_ShouldRegisterGetOnly()
{ {
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: ConfigureWeChatFallback); await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: ConfigureWeChatFallback);
var methods = host.GetHttpMethods("apiwechat/verification"); var methods = host.GetHttpMethods("apiwechat/verification");
methods.Should().Contain("GET"); methods.Should().BeEquivalentTo(["GET"]);
methods.Should().Contain("POST");
} }
[Fact] [Fact]
@ -61,14 +60,27 @@ public class WeChatRouteIntegrationTests
} }
[Fact] [Fact]
public async Task WeChatValidateLegacy_Head_ShouldReturn200() public async Task WeChatValidateLegacy_ShouldReturnEchostrAsPlainTextWithoutJsonWrapper()
{ {
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: ConfigureWeChatFallback); await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
{
ConfigureWeChatFallback(services);
services.PostConfigure<WeChatMpOptions>(options => options.Token = "test-token");
});
var request = new HttpRequestMessage(HttpMethod.Head, "/apiwechat/verification"); const string timestamp = "1400000000";
var response = await host.Client.SendAsync(request); const string nonce = "nonce";
const string echostr = "5938472619053847261";
var signature = SignWeChat("test-token", timestamp, nonce);
var response = await host.Client.GetAsync(
$"/apiwechat/verification?signature={signature}&timestamp={timestamp}&nonce={nonce}&echostr={echostr}");
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Content.Headers.ContentType?.MediaType.Should().Be("text/plain");
var body = await response.Content.ReadAsStringAsync();
body.Should().Be(echostr);
body.Should().NotStartWith("{");
} }
private static void ConfigureWeChatFallback(IServiceCollection services) private static void ConfigureWeChatFallback(IServiceCollection services)

View File

@ -61,7 +61,7 @@ public class WeChatScanLoginIntegrationTests
using var postContent = new StringContent(scanXml, Encoding.UTF8, "text/xml"); using var postContent = new StringContent(scanXml, Encoding.UTF8, "text/xml");
var postResponse = await host.Client.PostAsync( var postResponse = await host.Client.PostAsync(
$"/apiwechat/verification?signature={signature}&timestamp={timestamp}&nonce={nonce}", $"/wechat/mp/wx-test?signature={signature}&timestamp={timestamp}&nonce={nonce}",
postContent); postContent);
postResponse.StatusCode.Should().Be(HttpStatusCode.OK); postResponse.StatusCode.Should().Be(HttpStatusCode.OK);

View File

@ -23,42 +23,36 @@ public partial class AuthService
LazyServiceProvider.GetRequiredService<ILogger<AuthService>>(); LazyServiceProvider.GetRequiredService<ILogger<AuthService>>();
/// <summary> /// <summary>
/// 微信公众号接入验签(GET) /// 微信公众号接入验签,验签通过后原样返回 echostr。
/// </summary> /// </summary>
[AllowAnonymous] [AllowAnonymous]
[HttpGet("/apiwechat/verification")] [HttpGet("/apiwechat/verification")]
[HttpHead("/apiwechat/verification")]
[Produces("text/plain")] [Produces("text/plain")]
[ApiExplorerSettings(GroupName = "WeChat")] [ApiExplorerSettings(GroupName = "WeChat")]
public async Task<ContentResult> WeChatValidateLegacyAsync( public Task<ContentResult> WeChatValidateLegacyAsync(
[FromQuery] string? signature, [FromQuery] string? signature,
[FromQuery] string? timestamp, [FromQuery] string? timestamp,
[FromQuery] string? nonce, [FromQuery] string? nonce,
[FromQuery] string? echostr, [FromQuery] string? echostr,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default) =>
{ WeChatValidateCoreAsync(signature, timestamp, nonce, cancellationToken);
if (string.Equals(
httpContextAccessor.HttpContext?.Request.Method,
HttpMethods.Head,
StringComparison.OrdinalIgnoreCase))
{
return WeChatMpCallbackSupport.HeadOk();
}
return await WeChatValidateCoreAsync(signature, timestamp, nonce, echostr, cancellationToken);
}
private async Task<ContentResult> WeChatValidateCoreAsync( private async Task<ContentResult> WeChatValidateCoreAsync(
string? signature, string? signature,
string? timestamp, string? timestamp,
string? nonce, string? nonce,
string? echostr,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var request = httpContextAccessor.HttpContext?.Request;
if (request == null)
{
return WeChatMpCallbackSupport.PlainError();
}
try try
{ {
var options = await WeChatMpConfigProvider.GetAsync(cancellationToken); var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
return WeChatMpCallbackSupport.Validate(options, signature, timestamp, nonce, echostr); return WeChatMpCallbackSupport.Validate(options, request, signature, timestamp, nonce);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -68,7 +62,7 @@ public partial class AuthService
} }
/// <summary> /// <summary>
/// 微信服务器接入验签。 /// 微信服务器接入验签,验签通过后原样返回 echostr。
/// </summary> /// </summary>
[AllowAnonymous] [AllowAnonymous]
[HttpGet("/wechat/mp/{appId}")] [HttpGet("/wechat/mp/{appId}")]
@ -82,39 +76,11 @@ public partial class AuthService
[FromQuery] string? echostr, [FromQuery] string? echostr,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var request = httpContextAccessor.HttpContext?.Request
?? throw new BusinessException("无法读取微信验签请求");
var options = await WeChatMpConfigProvider.GetAsync(cancellationToken); var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
WeChatMpCallbackSupport.EnsureAppId(options, appId); WeChatMpCallbackSupport.EnsureAppId(options, appId);
return WeChatMpCallbackSupport.Validate(options, signature, timestamp, nonce, echostr); return WeChatMpCallbackSupport.Validate(options, request, signature, timestamp, nonce);
}
/// <summary>
/// 微信公众号消息/事件回调(POST)
/// </summary>
[AllowAnonymous]
[HttpPost("/apiwechat/verification")]
[Consumes("text/xml", "application/xml", "text/plain")]
[Produces("application/xml")]
[ApiExplorerSettings(GroupName = "WeChat")]
public async Task<ContentResult> WeChatMessageLegacyAsync(
[FromQuery] string signature,
[FromQuery] string timestamp,
[FromQuery] string nonce,
[FromQuery(Name = "encrypt_type")] string? encryptType,
CancellationToken cancellationToken = default)
{
var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
var request = httpContextAccessor.HttpContext?.Request
?? throw new BusinessException("无法读取微信回调请求");
return await WeChatMpCallbackSupport.HandleMessageAsync(
options,
WeChatEventHandler,
request,
WeChatLogger,
signature,
timestamp,
nonce,
encryptType,
cancellationToken);
} }
/// <summary> /// <summary>

View File

@ -9,10 +9,10 @@ internal static class WeChatMpCallbackSupport
{ {
public static ContentResult Validate( public static ContentResult Validate(
WeChatMpOptions options, WeChatMpOptions options,
HttpRequest request,
string? signature, string? signature,
string? timestamp, string? timestamp,
string? nonce, string? nonce)
string? echostr)
{ {
// 微信服务器要求 HTTP 200 + 纯文本 echostr,不可抛 BusinessException(会变成 JSON 非 200) // 微信服务器要求 HTTP 200 + 纯文本 echostr,不可抛 BusinessException(会变成 JSON 非 200)
if (string.IsNullOrWhiteSpace(options.Token)) if (string.IsNullOrWhiteSpace(options.Token))
@ -20,16 +20,21 @@ internal static class WeChatMpCallbackSupport
return PlainError(); return PlainError();
} }
var echostr = request.Query.TryGetValue("echostr", out var echostrValue)
? echostrValue.ToString()
: string.Empty;
if (string.IsNullOrWhiteSpace(signature) if (string.IsNullOrWhiteSpace(signature)
|| string.IsNullOrWhiteSpace(timestamp) || string.IsNullOrWhiteSpace(timestamp)
|| string.IsNullOrWhiteSpace(nonce) || string.IsNullOrWhiteSpace(nonce)
|| string.IsNullOrWhiteSpace(echostr)) || string.IsNullOrEmpty(echostr))
{ {
return PlainError(); return PlainError();
} }
var valid = WeChatSignatureHelper.Validate(options.Token, timestamp, nonce, signature); var valid = WeChatSignatureHelper.Validate(options.Token, timestamp, nonce, signature);
return PlainText(valid ? echostr : "error"); // 验签通过后原样返回 echostr,不可包 JSON、不可加引号或换行
return valid ? EchoStr(echostr) : PlainError();
} }
/// <summary>微信或探活可能发 HEAD,须返回 200(WMS/Spring 默认支持)。</summary> /// <summary>微信或探活可能发 HEAD,须返回 200(WMS/Spring 默认支持)。</summary>
@ -95,6 +100,9 @@ internal static class WeChatMpCallbackSupport
} }
} }
private static ContentResult EchoStr(string echostr) =>
new() { Content = echostr, ContentType = "text/plain", StatusCode = StatusCodes.Status200OK };
private static ContentResult PlainText(string content) => private static ContentResult PlainText(string content) =>
new() { Content = content, ContentType = "text/plain; charset=utf-8", StatusCode = StatusCodes.Status200OK }; new() { Content = content, ContentType = "text/plain; charset=utf-8", StatusCode = StatusCodes.Status200OK };