All checks were successful
Build and Deploy / deploy (push) Successful in 32s
ASP.NET 路由模板中方括号需转义为 [[ ]],否则应用在 InitializeApplication 阶段崩溃。
273 lines
10 KiB
C#
273 lines
10 KiB
C#
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");
|
||
}
|
||
}
|
||
}
|