From 8d7ba36fb400916e59e2a369433ec54d4ae93353 Mon Sep 17 00:00:00 2001 From: xRain Date: Tue, 6 Oct 2026 23:12:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20ISimApiTokenResolver=20?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89Token=E8=A7=A3=E6=9E=90=E6=89=A9?= =?UTF-8?q?=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetLogin 优先按注册顺序执行 TokenResolver 链, 非null即短路, 全部为null才回退内置缓存; resolver 每次调用在独立作用域内解析并即时释放 --- Helpers/SimApiAuth.cs | 13 ++++++++++++- Interfaces/ISimApiTokenResolver.cs | 31 ++++++++++++++++++++++++++++++ README.md | 29 +++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 Interfaces/ISimApiTokenResolver.cs diff --git a/Helpers/SimApiAuth.cs b/Helpers/SimApiAuth.cs index dc78ced..f3c036b 100644 --- a/Helpers/SimApiAuth.cs +++ b/Helpers/SimApiAuth.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; using SimApi.Communications; +using SimApi.Interfaces; using StackExchange.Redis; namespace SimApi.Helpers; @@ -18,14 +20,16 @@ public class SimApiAuth private readonly IDistributedCache _cache; private readonly IDatabase? _redisDb; + private readonly IServiceScopeFactory _scopeFactory; // InMemory 模式:用户 → Token集合 private readonly ConcurrentDictionary> _userTokens = new(); - public SimApiAuth(IDistributedCache cache, IServiceProvider sp) + public SimApiAuth(IDistributedCache cache, IServiceProvider sp, IServiceScopeFactory scopeFactory) { _cache = cache; _redisDb = sp.GetService()?.GetDatabase(); + _scopeFactory = scopeFactory; } /// @@ -88,6 +92,13 @@ public class SimApiAuth /// public SimApiLoginItem? GetLogin(string token) { + using var scope = _scopeFactory.CreateScope(); // 用完即释放 + foreach (var resolver in scope.ServiceProvider.GetServices()) + { + var item = resolver.Run(token); + if (item != null) return item; + } + var cacheKey = TokenCacheKey.Replace("{token}", token); var login = _cache.GetString(cacheKey); var resp = login != null ? SimApiUtil.FromJson(login) : null; diff --git a/Interfaces/ISimApiTokenResolver.cs b/Interfaces/ISimApiTokenResolver.cs new file mode 100644 index 0000000..7f64ca2 --- /dev/null +++ b/Interfaces/ISimApiTokenResolver.cs @@ -0,0 +1,31 @@ +using SimApi.Communications; + +namespace SimApi.Interfaces; + +/// +/// 自定义 Token 解析扩展点,用于让 SimApiAuth.GetLogin 识别本库之外签发的 Token(JWT、第三方登录态等)。 +/// +/// +/// 注册:本库不会自动发现实现,必须由使用者自行注册。AddScoped / AddSingleton / AddTransient +/// 均可,因为每次调用都在独立作用域内解析。仅在 EnableSimApiAuth = true 时才会被执行。
+/// 调用时机:每次 GetLogin(即每个携带 Token 的请求)都会先执行全部 resolver,再回退到内置缓存查询; +/// 因此本机 Login 签发的 Token 同样会走一遍 resolver 链。Logout 与 GetAllLogins 内部也调用 +/// GetLogin,同样会触发 resolver 链,其中 GetAllLogins 会按该用户的 Token 数量执行多次。
+/// 顺序:按注册顺序依次执行,返回非 null 即作为认证结果并立即短路,后续 resolver 与内置缓存不再参与。
+/// 生命周期:每次调用在根容器新建的子作用域内解析,因此可以安全注入 Scoped 服务(如 DbContext)。 +/// 但该作用域不是当前请求的作用域,请求级事务 / UnitOfWork 的未提交改动不可见,实现应当只读。
+/// 返回值:必须是完全物化的对象。作用域在返回后立即释放,不可返回依赖延迟加载的 EF 追踪实体。
+/// Token 管理:返回的登录态不受本库管理,Login / Update / GetAllLogins 不会包含它, +/// Logout 对它无效,吊销需由实现方自行处理。
+/// 异常:抛出的异常会直接冒泡并中断整条链(该请求认证失败)。表示“不是我的 Token”请返回 null,不要抛异常。
+/// 性能:这是认证热路径上的同步方法,查库等 I/O 会阻塞调用线程,建议在实现内部自行做短 TTL 缓存。 +///
+public interface ISimApiTokenResolver +{ + /// + /// 尝试解析 Token。 + /// + /// 请求携带的 Token 原文。 + /// 解析成功返回登录信息;不属于本实现的 Token 返回 null 由后续 resolver 继续处理。 + public SimApiLoginItem? Run(string token); +} diff --git a/README.md b/README.md index 5c87bb7..a9115ec 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ SimApi/ ├── Configurations/ # SimApiOptions + 各模块 Option 类 ├── Controllers/ # SimApiBaseController, SimApiCommonController, SimApiAuthController ├── Helpers/ # SimApiError, SimApiAuth, SimApiCache, SimApiHttpClient, SimApiStorage, SimApiUtil, SimApiAesUtil -├── Interfaces/ # ISimApiAuthChecker +├── Interfaces/ # ISimApiAuthChecker, ISimApiTokenResolver ├── Middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware ├── Synapse/ # MQTT Pub/Sub + RPC + Config Store ├── Exceptions/ # SimApiException @@ -221,6 +221,33 @@ public interface ISimApiAuthChecker { // 实现后自动注册为 Scoped,每次认证后调用 ``` +### 自定义 Token 解析 — ISimApiTokenResolver + +让认证识别**本库之外签发的 Token**(JWT、第三方登录态等),是 `GetLogin` 的前置扩展点。 + +```csharp +public interface ISimApiTokenResolver { + SimApiLoginItem? Run(string token); // 不是自己负责的 Token 返回 null +} +``` + +**必须由使用者手动注册**,本库不会自动发现实现(`AddScoped` / `AddSingleton` / `AddTransient` 均可): + +```csharp +builder.Services.AddScoped(); +``` + +调用行为: + +- 每次 `GetLogin`(即每个携带 Token 的请求)都会**先**按注册顺序执行全部 resolver,返回非 `null` 即短路生效;全部返回 `null` 才回退到内置缓存查询 +- 因此本机 `Login()` 签发的 Token 同样会走一遍 resolver 链,实现应保持廉价,或自行做短 TTL 缓存 +- `Logout` 与 `GetAllLogins` 内部也会调用 `GetLogin`,同样会触发 resolver 链,其中 `GetAllLogins` 会按该用户的 Token 数量执行多次 +- 每次调用在新建的子作用域内解析,可以注入 Scoped 服务(如 `DbContext`);但它**不是当前请求的作用域**,请求级事务 / UnitOfWork 的未提交改动不可见,实现应当只读 +- 返回值必须是完全物化的对象,作用域在返回后立即释放,不可返回依赖延迟加载的 EF 追踪实体 +- 返回的登录态**不受本库管理**:`Login` / `Update` / `GetAllLogins` 不会包含它,`Logout` 对它无效,吊销需由实现方自行处理 +- 抛出的异常会中断整条链并导致该请求认证失败;表示"不是我的 Token"请返回 `null`,不要抛异常 +- 仅在 `EnableSimApiAuth = true` 时才会被执行 + --- ## 4. AuthSDK — Auth Center 网关 & IAM