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
{
[Fact]
public async Task WeChatLegacyVerification_ShouldRegisterGetAndPostMethods()
public async Task WeChatLegacyVerification_ShouldRegisterGetOnly()
{
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: ConfigureWeChatFallback);
var methods = host.GetHttpMethods("apiwechat/verification");
methods.Should().Contain("GET");
methods.Should().Contain("POST");
methods.Should().BeEquivalentTo(["GET"]);
}
[Fact]
@ -61,14 +60,27 @@ public class WeChatRouteIntegrationTests
}
[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");
var response = await host.Client.SendAsync(request);
const string timestamp = "1400000000";
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.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)

View File

@ -61,7 +61,7 @@ public class WeChatScanLoginIntegrationTests
using var postContent = new StringContent(scanXml, Encoding.UTF8, "text/xml");
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);
postResponse.StatusCode.Should().Be(HttpStatusCode.OK);

View File

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

View File

@ -9,10 +9,10 @@ internal static class WeChatMpCallbackSupport
{
public static ContentResult Validate(
WeChatMpOptions options,
HttpRequest request,
string? signature,
string? timestamp,
string? nonce,
string? echostr)
string? nonce)
{
// 微信服务器要求 HTTP 200 + 纯文本 echostr,不可抛 BusinessException(会变成 JSON 非 200)
if (string.IsNullOrWhiteSpace(options.Token))
@ -20,16 +20,21 @@ internal static class WeChatMpCallbackSupport
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.IsNullOrWhiteSpace(echostr))
|| string.IsNullOrEmpty(echostr))
{
return PlainError();
}
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>
@ -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) =>
new() { Content = content, ContentType = "text/plain; charset=utf-8", StatusCode = StatusCodes.Status200OK };