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:
parent
0a5bd75618
commit
6c2e095196
@ -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}×tamp={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)
|
||||||
|
|||||||
@ -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}×tamp={timestamp}&nonce={nonce}",
|
$"/wechat/mp/wx-test?signature={signature}×tamp={timestamp}&nonce={nonce}",
|
||||||
postContent);
|
postContent);
|
||||||
|
|
||||||
postResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
postResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user