Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f2271ece8 | ||
|
|
6f293ddb22 | ||
|
|
b534ba9818 | ||
|
|
8d7ba36fb4 | ||
|
|
e4f051cb61 | ||
|
|
7b34991ea0 | ||
|
|
1623343748 |
@@ -438,3 +438,5 @@ MigrationBackup/
|
|||||||
# BitFun snapshot data - auto managed
|
# BitFun snapshot data - auto managed
|
||||||
.bitfun/
|
.bitfun/
|
||||||
/.bitfun/search/flashgrep-index/
|
/.bitfun/search/flashgrep-index/
|
||||||
|
.openbitfun/
|
||||||
|
/.openbitfun/search/flashgrep-index/
|
||||||
|
|||||||
@@ -16,4 +16,9 @@ public class SimApiRequestLogOptions
|
|||||||
/// 请求字段显示最长长度
|
/// 请求字段显示最长长度
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int RequestStringLogLength { get; set; } = 0;
|
public int RequestStringLogLength { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 响应字段显示最长长度
|
||||||
|
/// </summary>
|
||||||
|
public int ResponseStringLogLength { get; set; } = 0;
|
||||||
}
|
}
|
||||||
+12
-1
@@ -1,9 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using SimApi.Communications;
|
using SimApi.Communications;
|
||||||
|
using SimApi.Interfaces;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace SimApi.Helpers;
|
namespace SimApi.Helpers;
|
||||||
@@ -18,14 +20,16 @@ public class SimApiAuth
|
|||||||
|
|
||||||
private readonly IDistributedCache _cache;
|
private readonly IDistributedCache _cache;
|
||||||
private readonly IDatabase? _redisDb;
|
private readonly IDatabase? _redisDb;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
|
||||||
// InMemory 模式:用户 → Token集合
|
// InMemory 模式:用户 → Token集合
|
||||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, byte>> _userTokens = new();
|
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, byte>> _userTokens = new();
|
||||||
|
|
||||||
public SimApiAuth(IDistributedCache cache, IServiceProvider sp)
|
public SimApiAuth(IDistributedCache cache, IServiceProvider sp, IServiceScopeFactory scopeFactory)
|
||||||
{
|
{
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_redisDb = sp.GetService<IConnectionMultiplexer>()?.GetDatabase();
|
_redisDb = sp.GetService<IConnectionMultiplexer>()?.GetDatabase();
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -88,6 +92,13 @@ public class SimApiAuth
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public SimApiLoginItem? GetLogin(string token)
|
public SimApiLoginItem? GetLogin(string token)
|
||||||
{
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope(); // 用完即释放
|
||||||
|
foreach (var resolver in scope.ServiceProvider.GetServices<ISimApiTokenResolver>())
|
||||||
|
{
|
||||||
|
var item = resolver.Run(token);
|
||||||
|
if (item != null) return item;
|
||||||
|
}
|
||||||
|
|
||||||
var cacheKey = TokenCacheKey.Replace("{token}", token);
|
var cacheKey = TokenCacheKey.Replace("{token}", token);
|
||||||
var login = _cache.GetString(cacheKey);
|
var login = _cache.GetString(cacheKey);
|
||||||
var resp = login != null ? SimApiUtil.FromJson<SimApiLoginItem>(login) : null;
|
var resp = login != null ? SimApiUtil.FromJson<SimApiLoginItem>(login) : null;
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using SimApi.Communications;
|
||||||
|
|
||||||
|
namespace SimApi.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自定义 Token 解析扩展点,用于让 <c>SimApiAuth.GetLogin</c> 识别本库之外签发的 Token(JWT、第三方登录态等)。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <b>注册</b>:本库不会自动发现实现,必须由使用者自行注册。<c>AddScoped</c> / <c>AddSingleton</c> / <c>AddTransient</c>
|
||||||
|
/// 均可,因为每次调用都在独立作用域内解析。仅在 <c>EnableSimApiAuth = true</c> 时才会被执行。<br/>
|
||||||
|
/// <b>调用时机</b>:每次 <c>GetLogin</c>(即每个携带 Token 的请求)都会先执行全部 resolver,再回退到内置缓存查询;
|
||||||
|
/// 因此本机 <c>Login</c> 签发的 Token 同样会走一遍 resolver 链。<c>Logout</c> 与 <c>GetAllLogins</c> 内部也调用
|
||||||
|
/// <c>GetLogin</c>,同样会触发 resolver 链,其中 <c>GetAllLogins</c> 会按该用户的 Token 数量执行多次。<br/>
|
||||||
|
/// <b>顺序</b>:按注册顺序依次执行,返回非 <c>null</c> 即作为认证结果并立即短路,后续 resolver 与内置缓存不再参与。<br/>
|
||||||
|
/// <b>生命周期</b>:每次调用在根容器新建的子作用域内解析,因此可以安全注入 Scoped 服务(如 DbContext)。
|
||||||
|
/// 但该作用域不是当前请求的作用域,请求级事务 / UnitOfWork 的未提交改动不可见,实现应当只读。<br/>
|
||||||
|
/// <b>返回值</b>:必须是完全物化的对象。作用域在返回后立即释放,不可返回依赖延迟加载的 EF 追踪实体。<br/>
|
||||||
|
/// <b>Token 管理</b>:返回的登录态不受本库管理,<c>Login</c> / <c>Update</c> / <c>GetAllLogins</c> 不会包含它,
|
||||||
|
/// <c>Logout</c> 对它无效,吊销需由实现方自行处理。<br/>
|
||||||
|
/// <b>异常</b>:抛出的异常会直接冒泡并中断整条链(该请求认证失败)。表示“不是我的 Token”请返回 <c>null</c>,不要抛异常。<br/>
|
||||||
|
/// <b>性能</b>:这是认证热路径上的同步方法,查库等 I/O 会阻塞调用线程,建议在实现内部自行做短 TTL 缓存。
|
||||||
|
/// </remarks>
|
||||||
|
public interface ISimApiTokenResolver
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试解析 Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">请求携带的 Token 原文。</param>
|
||||||
|
/// <returns>解析成功返回登录信息;不属于本实现的 Token 返回 <c>null</c> 由后续 resolver 继续处理。</returns>
|
||||||
|
public SimApiLoginItem? Run(string token);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using System.Runtime.ExceptionServices;
|
using System.Runtime.ExceptionServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -61,35 +62,8 @@ public class SimApiRequestLogMiddleware(
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// GET/DELETE 等无 body 的请求, 或表单/文件上传等非 JSON body:
|
// GET/DELETE 等无 body 的请求, 或表单/文件上传等非 JSON body:
|
||||||
// 不进行长度裁剪, 原样记录, 避免空 JSON 解析抛异常
|
// 不做长度裁剪, 原样记录, 避免空 JSON 解析抛异常
|
||||||
Dictionary<string, object>? reqLogs = null;
|
logMessage.AppendLine(TrimJsonStringFields(requestBodyText, options.RequestStringLogLength));
|
||||||
try
|
|
||||||
{
|
|
||||||
reqLogs = SimApiUtil.FromJson<Dictionary<string, object>>(requestBodyText);
|
|
||||||
}
|
|
||||||
catch (JsonException)
|
|
||||||
{
|
|
||||||
reqLogs = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reqLogs == null)
|
|
||||||
{
|
|
||||||
logMessage.AppendLine(requestBodyText);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var reqLog in reqLogs)
|
|
||||||
{
|
|
||||||
if (reqLog.Value is not JsonElement { ValueKind: JsonValueKind.String } je) continue;
|
|
||||||
var str = je.GetString();
|
|
||||||
if (str?.Length > options.RequestStringLogLength)
|
|
||||||
{
|
|
||||||
reqLogs[reqLog.Key] = str[..options.RequestStringLogLength] + $"...({str.Length})";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logMessage.AppendLine(SimApiUtil.Json(reqLogs));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var originalBodyStream = context.Response.Body;
|
var originalBodyStream = context.Response.Body;
|
||||||
@@ -114,7 +88,12 @@ public class SimApiRequestLogMiddleware(
|
|||||||
var responseText = await new StreamReader(responseBody).ReadToEndAsync();
|
var responseText = await new StreamReader(responseBody).ReadToEndAsync();
|
||||||
|
|
||||||
logMessage.AppendLine($"*( Response [{context.Response.StatusCode}] ) =>");
|
logMessage.AppendLine($"*( Response [{context.Response.StatusCode}] ) =>");
|
||||||
if (options.ShowFullResponse)
|
if (options.ResponseStringLogLength > 0)
|
||||||
|
{
|
||||||
|
// 与请求体相同的字段级裁剪: JSON 按字段截断超长字符串, 非 JSON/空响应原样记录
|
||||||
|
logMessage.Append(TrimJsonStringFields(responseText, options.ResponseStringLogLength));
|
||||||
|
}
|
||||||
|
else if (options.ShowFullResponse)
|
||||||
{
|
{
|
||||||
logMessage.Append(responseText);
|
logMessage.Append(responseText);
|
||||||
}
|
}
|
||||||
@@ -139,4 +118,79 @@ public class SimApiRequestLogMiddleware(
|
|||||||
|
|
||||||
edi?.Throw();
|
edi?.Throw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 与请求体相同的字段级裁剪(递归):
|
||||||
|
/// 遍历 JSON 对象/数组的所有层级, 把长度超过 maxLength 的字符串字段截断为前 maxLength 字符并追加 ...(原长度);
|
||||||
|
/// 空 body 或非 JSON body(表单/文件上传等)原样返回, 不裁剪
|
||||||
|
/// </summary>
|
||||||
|
private static string TrimJsonStringFields(string text, int maxLength)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode? root;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
root = JsonNode.Parse(text);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root == null)
|
||||||
|
{
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrimNode(root, maxLength);
|
||||||
|
return SimApiUtil.Json(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 递归裁剪节点下所有层级的字符串值:
|
||||||
|
/// 纯字符串字面量超长时截断, 数字/布尔/嵌套对象/数组继续向下递归
|
||||||
|
/// </summary>
|
||||||
|
private static void TrimNode(JsonNode? node, int maxLength)
|
||||||
|
{
|
||||||
|
switch (node)
|
||||||
|
{
|
||||||
|
case JsonObject obj:
|
||||||
|
// 键快照遍历后再赋值, 避免枚举过程中修改集合引发异常
|
||||||
|
foreach (var prop in obj.ToList())
|
||||||
|
{
|
||||||
|
if (prop.Value is JsonValue value &&
|
||||||
|
value.TryGetValue<string>(out var str) &&
|
||||||
|
str.Length > maxLength)
|
||||||
|
{
|
||||||
|
obj[prop.Key] = JsonValue.Create(str[..maxLength] + $"...({str.Length})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TrimNode(prop.Value, maxLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case JsonArray arr:
|
||||||
|
for (var i = 0; i < arr.Count; i++)
|
||||||
|
{
|
||||||
|
if (arr[i] is JsonValue value &&
|
||||||
|
value.TryGetValue<string>(out var str) &&
|
||||||
|
str.Length > maxLength)
|
||||||
|
{
|
||||||
|
arr[i] = JsonValue.Create(str[..maxLength] + $"...({str.Length})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TrimNode(arr[i], maxLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ SimApi/
|
|||||||
├── Configurations/ # SimApiOptions + 各模块 Option 类
|
├── Configurations/ # SimApiOptions + 各模块 Option 类
|
||||||
├── Controllers/ # SimApiBaseController, SimApiCommonController, SimApiAuthController
|
├── Controllers/ # SimApiBaseController, SimApiCommonController, SimApiAuthController
|
||||||
├── Helpers/ # SimApiError, SimApiAuth, SimApiCache, SimApiHttpClient, SimApiStorage, SimApiUtil, SimApiAesUtil
|
├── Helpers/ # SimApiError, SimApiAuth, SimApiCache, SimApiHttpClient, SimApiStorage, SimApiUtil, SimApiAesUtil
|
||||||
├── Interfaces/ # ISimApiAuthChecker
|
├── Interfaces/ # ISimApiAuthChecker, ISimApiTokenResolver
|
||||||
├── Middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware
|
├── Middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware
|
||||||
├── Synapse/ # MQTT Pub/Sub + RPC + Config Store
|
├── Synapse/ # MQTT Pub/Sub + RPC + Config Store
|
||||||
├── Exceptions/ # SimApiException
|
├── Exceptions/ # SimApiException
|
||||||
@@ -221,6 +221,33 @@ public interface ISimApiAuthChecker {
|
|||||||
// 实现后自动注册为 Scoped,每次认证后调用
|
// 实现后自动注册为 Scoped,每次认证后调用
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 自定义 Token 解析 — ISimApiTokenResolver
|
||||||
|
|
||||||
|
让认证识别**本库之外签发的 Token**(JWT、第三方登录态等),是 `GetLogin` 的前置扩展点。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface ISimApiTokenResolver {
|
||||||
|
SimApiLoginItem? Run(string token); // 不是自己负责的 Token 返回 null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**必须由使用者手动注册**,本库不会自动发现实现(`AddScoped` / `AddSingleton` / `AddTransient` 均可):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddScoped<ISimApiTokenResolver, MyJwtTokenResolver>();
|
||||||
|
```
|
||||||
|
|
||||||
|
调用行为:
|
||||||
|
|
||||||
|
- 每次 `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
|
## 4. AuthSDK — Auth Center 网关 & IAM
|
||||||
|
|||||||
+3
-3
@@ -12,12 +12,12 @@
|
|||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.23" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.25" />
|
||||||
<PackageReference Include="Hangfire.Console" Version="1.4.3"/>
|
<PackageReference Include="Hangfire.Console" Version="1.4.3"/>
|
||||||
<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.12.0"/>
|
<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.12.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.12" />
|
||||||
<PackageReference Include="Minio" Version="7.0.0" />
|
<PackageReference Include="Minio" Version="7.0.0" />
|
||||||
<PackageReference Include="MQTTnet" Version="5.1.0.1559" />
|
<PackageReference Include="MQTTnet" Version="5.2.0.1603" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.2.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.2.3" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+1
-1
@@ -353,7 +353,7 @@ public static class SimApiExtensions
|
|||||||
builder.Configure<ForwardedHeadersOptions>(fwOptions =>
|
builder.Configure<ForwardedHeadersOptions>(fwOptions =>
|
||||||
{
|
{
|
||||||
fwOptions.ForwardedHeaders = ForwardedHeaders.All;
|
fwOptions.ForwardedHeaders = ForwardedHeaders.All;
|
||||||
fwOptions.KnownIPNetworks.Clear();
|
fwOptions.KnownNetworks.Clear();
|
||||||
fwOptions.KnownProxies.Clear();
|
fwOptions.KnownProxies.Clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user