fix: 微信回调路由挂到 AuthService 以修复 404
Fake 框架未注册 SimpleAdmin.Api.WeChat 下的 ApplicationService, 将 /apiwechat/verification 等公众号验签与消息接口并入 AuthService。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
26ccc69dc6
commit
42b41e68c7
@ -0,0 +1,56 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SimpleAdmin.Api.IntegrationTests.Support;
|
||||||
|
using SimpleAdmin.Api.WeChat.Options;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Api.IntegrationTests;
|
||||||
|
|
||||||
|
public class WeChatRouteIntegrationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task WeChatRoutes_ShouldBeRegisteredOnAuthService()
|
||||||
|
{
|
||||||
|
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
|
||||||
|
{
|
||||||
|
services.PostConfigure<WeChatMpOptions>(options =>
|
||||||
|
{
|
||||||
|
options.Token = "test-token";
|
||||||
|
options.AppId = "wx-test";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
host.RoutePatterns.Should().Contain("apiwechat/verification");
|
||||||
|
host.RoutePatterns.Should().Contain("wechat/mp/{appId}");
|
||||||
|
host.RoutePatterns.Should().Contain("wechat/mp/login-qrcode");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WeChatValidateLegacy_ShouldReturnEchostrWhenSignatureValid()
|
||||||
|
{
|
||||||
|
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
|
||||||
|
{
|
||||||
|
services.PostConfigure<WeChatMpOptions>(options => options.Token = "test-token");
|
||||||
|
});
|
||||||
|
|
||||||
|
const string timestamp = "1400000000";
|
||||||
|
const string nonce = "nonce";
|
||||||
|
const string echostr = "hello-wechat";
|
||||||
|
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);
|
||||||
|
(await response.Content.ReadAsStringAsync()).Should().Be(echostr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SignWeChat(string token, string timestamp, string nonce)
|
||||||
|
{
|
||||||
|
var parts = new[] { token, timestamp, nonce }.OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||||
|
var hash = SHA1.HashData(Encoding.UTF8.GetBytes(string.Concat(parts)));
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/SimpleAdmin.Api/Auth/AuthService.WeChatMp.cs
Normal file
145
src/SimpleAdmin.Api/Auth/AuthService.WeChatMp.cs
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
using Fake.Application;
|
||||||
|
using Fake.Domain.Exceptions;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SimpleAdmin.Api.WeChat;
|
||||||
|
using SimpleAdmin.Api.WeChat.Dtos;
|
||||||
|
using SimpleAdmin.Api.WeChat.Options;
|
||||||
|
using SimpleAdmin.Api.WeChat.Services;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Api.Auth;
|
||||||
|
|
||||||
|
public partial class AuthService
|
||||||
|
{
|
||||||
|
private IOptions<WeChatMpOptions> WeChatMpOptions =>
|
||||||
|
LazyServiceProvider.GetRequiredService<IOptions<WeChatMpOptions>>();
|
||||||
|
|
||||||
|
private IWeChatMpApiClient WeChatMpApiClient =>
|
||||||
|
LazyServiceProvider.GetRequiredService<IWeChatMpApiClient>();
|
||||||
|
|
||||||
|
private IWeChatEventHandler WeChatEventHandler =>
|
||||||
|
LazyServiceProvider.GetRequiredService<IWeChatEventHandler>();
|
||||||
|
|
||||||
|
private ILogger<AuthService> WeChatLogger =>
|
||||||
|
LazyServiceProvider.GetRequiredService<ILogger<AuthService>>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信服务器接入验签(兼容 WMS:/api/apiwechat/verification)。
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("/apiwechat/verification")]
|
||||||
|
[Produces("text/plain")]
|
||||||
|
[ApiExplorerSettings(GroupName = "WeChat")]
|
||||||
|
public ContentResult WeChatValidateLegacyAsync(
|
||||||
|
[FromQuery] string? signature,
|
||||||
|
[FromQuery] string? timestamp,
|
||||||
|
[FromQuery] string? nonce,
|
||||||
|
[FromQuery] string? echostr)
|
||||||
|
=> WeChatMpCallbackSupport.Validate(WeChatMpOptions, signature, timestamp, nonce, echostr);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信服务器接入验签。
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("/wechat/mp/{appId}")]
|
||||||
|
[Produces("text/plain")]
|
||||||
|
[ApiExplorerSettings(GroupName = "WeChat")]
|
||||||
|
public ContentResult WeChatValidateAsync(
|
||||||
|
[FromRoute] string appId,
|
||||||
|
[FromQuery] string? signature,
|
||||||
|
[FromQuery] string? timestamp,
|
||||||
|
[FromQuery] string? nonce,
|
||||||
|
[FromQuery] string? echostr)
|
||||||
|
{
|
||||||
|
WeChatMpCallbackSupport.EnsureAppId(WeChatMpOptions, appId);
|
||||||
|
return WeChatMpCallbackSupport.Validate(WeChatMpOptions, signature, timestamp, nonce, echostr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信消息/事件回调(兼容 WMS:/api/apiwechat/verification)。
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("/apiwechat/verification")]
|
||||||
|
[Consumes("text/xml", "application/xml", "text/plain")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
[ApiExplorerSettings(GroupName = "WeChat")]
|
||||||
|
public Task<ContentResult> WeChatMessageLegacyAsync(
|
||||||
|
[FromQuery] string signature,
|
||||||
|
[FromQuery] string timestamp,
|
||||||
|
[FromQuery] string nonce,
|
||||||
|
[FromQuery(Name = "encrypt_type")] string? encryptType,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var request = httpContextAccessor.HttpContext?.Request
|
||||||
|
?? throw new BusinessException("无法读取微信回调请求");
|
||||||
|
return WeChatMpCallbackSupport.HandleMessageAsync(
|
||||||
|
WeChatMpOptions,
|
||||||
|
WeChatEventHandler,
|
||||||
|
request,
|
||||||
|
WeChatLogger,
|
||||||
|
signature,
|
||||||
|
timestamp,
|
||||||
|
nonce,
|
||||||
|
encryptType,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信消息/事件回调。
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("/wechat/mp/{appId}")]
|
||||||
|
[Consumes("text/xml", "application/xml", "text/plain")]
|
||||||
|
[Produces("application/xml")]
|
||||||
|
[ApiExplorerSettings(GroupName = "WeChat")]
|
||||||
|
public async Task<ContentResult> WeChatMessageAsync(
|
||||||
|
[FromRoute] string appId,
|
||||||
|
[FromQuery] string signature,
|
||||||
|
[FromQuery] string timestamp,
|
||||||
|
[FromQuery] string nonce,
|
||||||
|
[FromQuery(Name = "encrypt_type")] string? encryptType,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
WeChatMpCallbackSupport.EnsureAppId(WeChatMpOptions, appId);
|
||||||
|
var request = httpContextAccessor.HttpContext?.Request
|
||||||
|
?? throw new BusinessException("无法读取微信回调请求");
|
||||||
|
return await WeChatMpCallbackSupport.HandleMessageAsync(
|
||||||
|
WeChatMpOptions,
|
||||||
|
WeChatEventHandler,
|
||||||
|
request,
|
||||||
|
WeChatLogger,
|
||||||
|
signature,
|
||||||
|
timestamp,
|
||||||
|
nonce,
|
||||||
|
encryptType,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 PC 扫码登录二维码。
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("/wechat/mp/login-qrcode")]
|
||||||
|
[ApiExplorerSettings(GroupName = "WeChat")]
|
||||||
|
public async Task<WxLoginQrcodeOutput> GetWeChatLoginQrcodeAsync(
|
||||||
|
[FromQuery] string loginUuid,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(loginUuid))
|
||||||
|
{
|
||||||
|
throw new BusinessException("缺少 loginUuid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const int expireSeconds = 600;
|
||||||
|
var qrCodeUrl = await WeChatMpApiClient.CreateLoginQrCodeUrlAsync(
|
||||||
|
loginUuid.Trim(),
|
||||||
|
expireSeconds,
|
||||||
|
cancellationToken);
|
||||||
|
return new WxLoginQrcodeOutput
|
||||||
|
{
|
||||||
|
QrCodeUrl = qrCodeUrl,
|
||||||
|
ExpiresIn = expireSeconds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,7 +29,7 @@ namespace SimpleAdmin.Api.Auth;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ApiExplorerSettings(GroupName = "RBAC")]
|
[ApiExplorerSettings(GroupName = "RBAC")]
|
||||||
public class AuthService(
|
public partial class AuthService(
|
||||||
AccountManager accountManager,
|
AccountManager accountManager,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
MenuService menuService,
|
MenuService menuService,
|
||||||
|
|||||||
@ -50,10 +50,6 @@ public class SimpleAdminApiModule : FakeModule
|
|||||||
settings.RootPath = "rbac";
|
settings.RootPath = "rbac";
|
||||||
settings.TypePredicate = type => type == typeof(AuthService);
|
settings.TypePredicate = type => type == typeof(AuthService);
|
||||||
});
|
});
|
||||||
options.ApplicationServices2Controller<SimpleAdminApiModule>(settings =>
|
|
||||||
{
|
|
||||||
settings.TypePredicate = type => type.Namespace == "SimpleAdmin.Api.WeChat";
|
|
||||||
});
|
|
||||||
options.ApplicationServices2Controller<FakeRbacApplicationModule>(settings =>
|
options.ApplicationServices2Controller<FakeRbacApplicationModule>(settings =>
|
||||||
{
|
{
|
||||||
settings.RootPath = "rbac";
|
settings.RootPath = "rbac";
|
||||||
|
|||||||
@ -5,6 +5,14 @@ namespace SimpleAdmin.Api.WeChat.Services;
|
|||||||
|
|
||||||
internal static class WeChatSignatureHelper
|
internal static class WeChatSignatureHelper
|
||||||
{
|
{
|
||||||
|
public static string Sign(string token, string timestamp, string nonce)
|
||||||
|
{
|
||||||
|
var parts = new[] { token, timestamp, nonce }.OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||||
|
var raw = string.Concat(parts);
|
||||||
|
var hash = SHA1.HashData(Encoding.UTF8.GetBytes(raw));
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
public static bool Validate(string token, string timestamp, string nonce, string signature)
|
public static bool Validate(string token, string timestamp, string nonce, string signature)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(token)
|
if (string.IsNullOrWhiteSpace(token)
|
||||||
|
|||||||
@ -21,7 +21,7 @@ namespace SimpleAdmin.Api.WeChat;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[ApiExplorerSettings(GroupName = "WeChat")]
|
[ApiExplorerSettings(GroupName = "WeChat")]
|
||||||
public class WeChatCustomerAppService(
|
public class WeChatCustomerService(
|
||||||
IWeChatMpApiClient weChatMpApiClient,
|
IWeChatMpApiClient weChatMpApiClient,
|
||||||
ICustomerRepository customerRepository,
|
ICustomerRepository customerRepository,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
@ -1,188 +0,0 @@
|
|||||||
using Fake.Application;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using SimpleAdmin.Api.WeChat.Dtos;
|
|
||||||
using SimpleAdmin.Api.WeChat.Options;
|
|
||||||
using SimpleAdmin.Api.WeChat.Services;
|
|
||||||
|
|
||||||
namespace SimpleAdmin.Api.WeChat;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 微信公众号服务器回调与登录二维码。
|
|
||||||
/// </summary>
|
|
||||||
[AllowAnonymous]
|
|
||||||
[ApiExplorerSettings(GroupName = "WeChat")]
|
|
||||||
public class WeChatMpAppService(
|
|
||||||
IOptions<WeChatMpOptions> options,
|
|
||||||
IWeChatMpApiClient weChatMpApiClient,
|
|
||||||
IWeChatEventHandler eventHandler,
|
|
||||||
IHttpContextAccessor httpContextAccessor,
|
|
||||||
ILogger<WeChatMpAppService> logger) : ApplicationService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 微信服务器接入验签(兼容 WMS 公众号菜单配置:/api/apiwechat/verification)。
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("/apiwechat/verification")]
|
|
||||||
[Produces("text/plain")]
|
|
||||||
public ContentResult ValidateLegacyAsync(
|
|
||||||
[FromQuery] string? signature,
|
|
||||||
[FromQuery] string? timestamp,
|
|
||||||
[FromQuery] string? nonce,
|
|
||||||
[FromQuery] string? echostr)
|
|
||||||
=> ValidateCore(signature, timestamp, nonce, echostr);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 微信服务器接入验签。
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("/wechat/mp/{appId}")]
|
|
||||||
[Produces("text/plain")]
|
|
||||||
public ContentResult ValidateAsync(
|
|
||||||
[FromRoute] string appId,
|
|
||||||
[FromQuery] string? signature,
|
|
||||||
[FromQuery] string? timestamp,
|
|
||||||
[FromQuery] string? nonce,
|
|
||||||
[FromQuery] string? echostr)
|
|
||||||
{
|
|
||||||
EnsureAppId(appId);
|
|
||||||
return ValidateCore(signature, timestamp, nonce, echostr);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 微信消息/事件回调(兼容 WMS:/api/apiwechat/verification)。
|
|
||||||
/// </summary>
|
|
||||||
[HttpPost("/apiwechat/verification")]
|
|
||||||
[Consumes("text/xml", "application/xml", "text/plain")]
|
|
||||||
[Produces("application/xml")]
|
|
||||||
public Task<ContentResult> MessageLegacyAsync(
|
|
||||||
[FromQuery] string signature,
|
|
||||||
[FromQuery] string timestamp,
|
|
||||||
[FromQuery] string nonce,
|
|
||||||
[FromQuery] string? openid,
|
|
||||||
[FromQuery(Name = "encrypt_type")] string? encryptType,
|
|
||||||
[FromQuery(Name = "msg_signature")] string? msgSignature,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
=> MessageCoreAsync(signature, timestamp, nonce, encryptType, cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 微信消息/事件回调。
|
|
||||||
/// </summary>
|
|
||||||
[HttpPost("/wechat/mp/{appId}")]
|
|
||||||
[Consumes("text/xml", "application/xml", "text/plain")]
|
|
||||||
[Produces("application/xml")]
|
|
||||||
public async Task<ContentResult> MessageAsync(
|
|
||||||
[FromRoute] string appId,
|
|
||||||
[FromQuery] string signature,
|
|
||||||
[FromQuery] string timestamp,
|
|
||||||
[FromQuery] string nonce,
|
|
||||||
[FromQuery] string? openid,
|
|
||||||
[FromQuery(Name = "encrypt_type")] string? encryptType,
|
|
||||||
[FromQuery(Name = "msg_signature")] string? msgSignature,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
EnsureAppId(appId);
|
|
||||||
return await MessageCoreAsync(signature, timestamp, nonce, encryptType, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ContentResult ValidateCore(
|
|
||||||
string? signature,
|
|
||||||
string? timestamp,
|
|
||||||
string? nonce,
|
|
||||||
string? echostr)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(options.Value.Token))
|
|
||||||
{
|
|
||||||
throw new BusinessException("未配置微信公众号 Token");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(signature)
|
|
||||||
|| string.IsNullOrWhiteSpace(timestamp)
|
|
||||||
|| string.IsNullOrWhiteSpace(nonce)
|
|
||||||
|| string.IsNullOrWhiteSpace(echostr))
|
|
||||||
{
|
|
||||||
return new ContentResult { Content = "error", ContentType = "text/plain; charset=utf-8" };
|
|
||||||
}
|
|
||||||
|
|
||||||
var valid = WeChatSignatureHelper.Validate(options.Value.Token, timestamp, nonce, signature);
|
|
||||||
return new ContentResult
|
|
||||||
{
|
|
||||||
Content = valid ? echostr : "error",
|
|
||||||
ContentType = "text/plain; charset=utf-8"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<ContentResult> MessageCoreAsync(
|
|
||||||
string signature,
|
|
||||||
string timestamp,
|
|
||||||
string nonce,
|
|
||||||
string? encryptType,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (!WeChatSignatureHelper.Validate(options.Value.Token, timestamp, nonce, signature))
|
|
||||||
{
|
|
||||||
logger.LogWarning("微信消息验签失败");
|
|
||||||
return new ContentResult { Content = "error", ContentType = "text/plain; charset=utf-8" };
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = httpContextAccessor.HttpContext?.Request
|
|
||||||
?? throw new BusinessException("无法读取微信回调请求");
|
|
||||||
using var reader = new StreamReader(request.Body);
|
|
||||||
var requestBody = await reader.ReadToEndAsync(cancellationToken);
|
|
||||||
if (string.IsNullOrWhiteSpace(requestBody))
|
|
||||||
{
|
|
||||||
return new ContentResult { Content = string.Empty, ContentType = "application/xml; charset=utf-8" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(encryptType)
|
|
||||||
&& !string.Equals(encryptType, "raw", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
logger.LogWarning("收到加密微信消息({EncryptType}),当前仅支持明文模式,请在公众平台切换为明文或后续启用 AES。", encryptType);
|
|
||||||
return new ContentResult { Content = string.Empty, ContentType = "application/xml; charset=utf-8" };
|
|
||||||
}
|
|
||||||
|
|
||||||
var message = WeChatXmlMessageParser.Parse(requestBody);
|
|
||||||
if (message == null)
|
|
||||||
{
|
|
||||||
return new ContentResult { Content = string.Empty, ContentType = "application/xml; charset=utf-8" };
|
|
||||||
}
|
|
||||||
|
|
||||||
var reply = await eventHandler.HandleAsync(message, cancellationToken);
|
|
||||||
return new ContentResult { Content = reply ?? string.Empty, ContentType = "application/xml; charset=utf-8" };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 PC 扫码登录二维码。
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("/wechat/mp/login-qrcode")]
|
|
||||||
public async Task<WxLoginQrcodeOutput> GetLoginQrcodeAsync(
|
|
||||||
[FromQuery] string loginUuid,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(loginUuid))
|
|
||||||
{
|
|
||||||
throw new BusinessException("缺少 loginUuid");
|
|
||||||
}
|
|
||||||
|
|
||||||
const int expireSeconds = 600;
|
|
||||||
var qrCodeUrl = await weChatMpApiClient.CreateLoginQrCodeUrlAsync(loginUuid.Trim(), expireSeconds, cancellationToken);
|
|
||||||
return new WxLoginQrcodeOutput
|
|
||||||
{
|
|
||||||
QrCodeUrl = qrCodeUrl,
|
|
||||||
ExpiresIn = expireSeconds
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureAppId(string appId)
|
|
||||||
{
|
|
||||||
var configured = options.Value.AppId;
|
|
||||||
if (string.IsNullOrWhiteSpace(configured))
|
|
||||||
{
|
|
||||||
throw new BusinessException("未配置微信公众号 AppId");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(configured, appId, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
throw new BusinessException("公众号 AppId 不匹配");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
97
src/SimpleAdmin.Api/WeChat/WeChatMpCallbackSupport.cs
Normal file
97
src/SimpleAdmin.Api/WeChat/WeChatMpCallbackSupport.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
using Fake.Application;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SimpleAdmin.Api.WeChat.Options;
|
||||||
|
using SimpleAdmin.Api.WeChat.Services;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Api.WeChat;
|
||||||
|
|
||||||
|
internal static class WeChatMpCallbackSupport
|
||||||
|
{
|
||||||
|
public static ContentResult Validate(
|
||||||
|
IOptions<WeChatMpOptions> options,
|
||||||
|
string? signature,
|
||||||
|
string? timestamp,
|
||||||
|
string? nonce,
|
||||||
|
string? echostr)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(options.Value.Token))
|
||||||
|
{
|
||||||
|
throw new BusinessException("未配置微信公众号 Token");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(signature)
|
||||||
|
|| string.IsNullOrWhiteSpace(timestamp)
|
||||||
|
|| string.IsNullOrWhiteSpace(nonce)
|
||||||
|
|| string.IsNullOrWhiteSpace(echostr))
|
||||||
|
{
|
||||||
|
return PlainText("error");
|
||||||
|
}
|
||||||
|
|
||||||
|
var valid = WeChatSignatureHelper.Validate(options.Value.Token, timestamp, nonce, signature);
|
||||||
|
return PlainText(valid ? echostr : "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ContentResult> HandleMessageAsync(
|
||||||
|
IOptions<WeChatMpOptions> options,
|
||||||
|
IWeChatEventHandler eventHandler,
|
||||||
|
HttpRequest request,
|
||||||
|
ILogger logger,
|
||||||
|
string signature,
|
||||||
|
string timestamp,
|
||||||
|
string nonce,
|
||||||
|
string? encryptType,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!WeChatSignatureHelper.Validate(options.Value.Token, timestamp, nonce, signature))
|
||||||
|
{
|
||||||
|
logger.LogWarning("微信消息验签失败");
|
||||||
|
return PlainText("error");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = new StreamReader(request.Body);
|
||||||
|
var requestBody = await reader.ReadToEndAsync(cancellationToken);
|
||||||
|
if (string.IsNullOrWhiteSpace(requestBody))
|
||||||
|
{
|
||||||
|
return Xml(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(encryptType)
|
||||||
|
&& !string.Equals(encryptType, "raw", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"收到加密微信消息({EncryptType}),当前仅支持明文模式,请在公众平台切换为明文或后续启用 AES。",
|
||||||
|
encryptType);
|
||||||
|
return Xml(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = WeChatXmlMessageParser.Parse(requestBody);
|
||||||
|
if (message == null)
|
||||||
|
{
|
||||||
|
return Xml(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply = await eventHandler.HandleAsync(message, cancellationToken);
|
||||||
|
return Xml(reply ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EnsureAppId(IOptions<WeChatMpOptions> options, string appId)
|
||||||
|
{
|
||||||
|
var configured = options.Value.AppId;
|
||||||
|
if (string.IsNullOrWhiteSpace(configured))
|
||||||
|
{
|
||||||
|
throw new BusinessException("未配置微信公众号 AppId");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(configured, appId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new BusinessException("公众号 AppId 不匹配");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ContentResult PlainText(string content) =>
|
||||||
|
new() { Content = content, ContentType = "text/plain; charset=utf-8" };
|
||||||
|
|
||||||
|
private static ContentResult Xml(string content) =>
|
||||||
|
new() { Content = content, ContentType = "application/xml; charset=utf-8" };
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user