fix(微信): 加固 GET 验签,直接输出 echostr 原文
- 从 Query 读取 signature/timestamp/nonce/echostr 并记录失败原因 - 验签通过后 WriteAsync 写回 echostr,避免 JSON 包装 - 补充生产 Token 验签集成测试 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
da1227d8aa
commit
991ad49255
@ -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}×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<WeChatMpOptions>(options =>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user