admin/src/SimpleAdmin.Api/Auth/AuthService.WeChatMp.cs
zzh 0a16f27664
All checks were successful
Build and Deploy / deploy (push) Successful in 32s
fix: 修复微信 mp 路由 regex 导致生产启动失败
ASP.NET 路由模板中方括号需转义为 [[ ]],否则应用在 InitializeApplication 阶段崩溃。
2026-09-09 15:31:54 +08:00

273 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Fake.Application;
using Fake.Domain.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SimpleAdmin.Api.WeChat;
using SimpleAdmin.Api.WeChat.Dtos;
using SimpleAdmin.Api.WeChat.Services;
namespace SimpleAdmin.Api.Auth;
public partial class AuthService
{
private IWeChatMpConfigProvider WeChatMpConfigProvider =>
LazyServiceProvider.GetRequiredService<IWeChatMpConfigProvider>();
private IWeChatMpApiClient WeChatMpApiClient =>
LazyServiceProvider.GetRequiredService<IWeChatMpApiClient>();
private IWeChatEventHandler WeChatEventHandler =>
LazyServiceProvider.GetRequiredService<IWeChatEventHandler>();
private ILogger<AuthService> WeChatLogger =>
LazyServiceProvider.GetRequiredService<ILogger<AuthService>>();
/// <summary>
/// 微信公众号接入验签,验签通过后原样返回 echostr。
/// </summary>
[AllowAnonymous]
[HttpGet("/apiwechat/verification")]
[Produces("text/plain")]
[ApiExplorerSettings(GroupName = "WeChat")]
public Task<IActionResult> WeChatValidateLegacyAsync(
[FromQuery] string? signature,
[FromQuery] string? timestamp,
[FromQuery] string? nonce,
[FromQuery] string? echostr,
CancellationToken cancellationToken = default) =>
WeChatValidateCoreAsync(cancellationToken);
private async Task<IActionResult> WeChatValidateCoreAsync(CancellationToken cancellationToken)
{
var httpContext = httpContextAccessor.HttpContext;
var request = httpContext?.Request;
if (request == null)
{
return WeChatMpCallbackSupport.PlainError();
}
try
{
var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
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)
{
WeChatLogger.LogWarning(ex, "微信公众号验签读取配置失败");
return WeChatMpCallbackSupport.PlainError();
}
}
/// <summary>
/// 微信服务器接入验签,验签通过后原样返回 echostr。
/// appId 仅匹配 wx 开头,避免 login-qrcode / oauth-config 等静态段落入本路由。
/// </summary>
[AllowAnonymous]
[HttpGet("/wechat/mp/{appId:regex(^wx[[a-zA-Z0-9]]+$)}")]
[Produces("text/plain")]
[ApiExplorerSettings(GroupName = "WeChat")]
public async Task<IActionResult> WeChatValidateAsync(
[FromRoute] string appId,
[FromQuery] string? signature,
[FromQuery] string? timestamp,
[FromQuery] string? nonce,
[FromQuery] string? echostr,
CancellationToken cancellationToken = default)
{
var httpContext = httpContextAccessor.HttpContext
?? throw new BusinessException("无法读取微信验签请求");
var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
WeChatMpCallbackSupport.EnsureAppId(options, appId);
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>
/// 微信公众号消息/事件推送(明文,XML/JSON)。
/// </summary>
[AllowAnonymous]
[HttpPost("/apiwechat/verification")]
[Consumes("text/xml", "application/xml", "application/json", "text/plain")]
[Produces("text/plain", "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) =>
WeChatMessageCoreAsync(signature, timestamp, nonce, encryptType, cancellationToken);
/// <summary>
/// 微信消息/事件推送(明文,XML/JSON)。
/// </summary>
[AllowAnonymous]
[HttpPost("/wechat/mp/{appId:regex(^wx[[a-zA-Z0-9]]+$)}")]
[Consumes("text/xml", "application/xml", "application/json", "text/plain")]
[Produces("text/plain", "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)
{
var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
WeChatMpCallbackSupport.EnsureAppId(options, appId);
return await WeChatMessageCoreAsync(signature, timestamp, nonce, encryptType, cancellationToken);
}
private async Task<ContentResult> WeChatMessageCoreAsync(
string signature,
string timestamp,
string nonce,
string? encryptType,
CancellationToken cancellationToken)
{
var request = httpContextAccessor.HttpContext?.Request
?? throw new BusinessException("无法读取微信回调请求");
var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
return await WeChatMpCallbackSupport.HandleMessageAsync(
options,
WeChatEventHandler,
request,
WeChatLogger,
signature,
timestamp,
nonce,
encryptType,
cancellationToken);
}
/// <summary>
/// H5 网页授权公开配置:AppId + OAuth 回调根(来自其他配置 wechat_mp)。
/// 注意:勿挂在 /wechat/mp/{appId} 下,避免被公众号回调路由吞掉。
/// </summary>
[AllowAnonymous]
[HttpGet("/wechat/oauth/config")]
[ApiExplorerSettings(GroupName = "WeChat")]
public async Task<WxOAuthConfigOutput> GetWeChatOAuthConfigAsync(
CancellationToken cancellationToken = default)
{
var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(options.AppId))
{
throw new BusinessException(
"未配置微信公众号 AppId。请在「其他配置」维护 wechat_mp,AppKey 填公众号 AppId。");
}
return new WxOAuthConfigOutput
{
AppId = options.AppId.Trim(),
OAuthRedirectBase = string.IsNullOrWhiteSpace(options.OAuthRedirectBase)
? null
: options.OAuthRedirectBase.Trim().TrimEnd('/')
};
}
/// <summary>
/// 获取 PC 扫码登录二维码。
/// </summary>
[AllowAnonymous]
[HttpGet("/wechat/mp/login-qrcode")]
[ApiExplorerSettings(GroupName = "WeChat")]
public async Task<WxLoginQrcodeOutput> GetWeChatLoginQrcodeAsync(
[FromQuery] string? loginUuid,
CancellationToken cancellationToken = default)
{
var loginKey = string.IsNullOrWhiteSpace(loginUuid)
? Guid.NewGuid().ToString("N")
: loginUuid.Trim();
const int expireSeconds = 600;
var qrCodeUrl = await WeChatMpApiClient.CreateLoginQrCodeUrlAsync(
loginKey,
expireSeconds,
cancellationToken);
return new WxLoginQrcodeOutput
{
LoginKey = loginKey,
QrCodeUrl = qrCodeUrl,
ExpiresIn = expireSeconds
};
}
/// <summary>
/// 获取注册/绑定成功后跳转的公众号链接(微信内直接打开即可关注,无需长按扫码)。
/// </summary>
[AllowAnonymous]
[HttpGet("/wechat/mp/follow-qrcode")]
[ApiExplorerSettings(GroupName = "WeChat")]
public async Task<WxFollowQrcodeOutput> GetWeChatFollowQrcodeAsync(
CancellationToken cancellationToken = default)
{
var jumpUrl = await WeChatMpConfigProvider.GetFollowJumpUrlAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(jumpUrl))
{
throw new BusinessException(
"未配置公众号关注跳转链接。请在「其他配置」新增 wechat_mp_follow,BaseUrl 填公众号主页/名片链接。");
}
return new WxFollowQrcodeOutput
{
JumpUrl = jumpUrl,
QrCodeUrl = jumpUrl,
Scene = WeChatQrSceneParser.FollowScene
};
}
/// <summary>
/// 同源代理微信 showqrcode 图片,便于微信内长按识别(避免跨域直连 mp.weixin.qq.com)。
/// </summary>
[AllowAnonymous]
[HttpGet("/wechat/mp/showqrcode")]
[ApiExplorerSettings(GroupName = "WeChat")]
public async Task<IActionResult> GetWeChatShowQrCodeAsync(
[FromQuery] string ticket,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(ticket))
{
return new BadRequestObjectResult("ticket required");
}
try
{
var (bytes, contentType) = await WeChatMpApiClient.DownloadShowQrCodeAsync(
ticket,
cancellationToken);
// 短缓存:同一次 ticket 有效期内可复用,减少重复拉取
return new FileContentResult(bytes, contentType)
{
EnableRangeProcessing = false
};
}
catch (Exception ex)
{
WeChatLogger.LogWarning(ex, "代理下载微信登录二维码失败");
return new NotFoundObjectResult("qrcode not found");
}
}
}