Compare commits
4
Commits
10.26.06.11
...
10.26.8.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7a4395000 | ||
|
|
bf98792f96 | ||
|
|
8c5081d442 | ||
|
|
2ed4605a2c |
@@ -437,3 +437,4 @@ MigrationBackup/
|
||||
|
||||
# BitFun snapshot data - auto managed
|
||||
.bitfun/
|
||||
/.bitfun/search/flashgrep-index/
|
||||
|
||||
@@ -51,7 +51,7 @@ public class SimApiBaseResponse(int code = 200, string message = "成功")
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class PageResponse<T>
|
||||
{
|
||||
public T? List { get; set; }
|
||||
public T[] List { get; set; } = [];
|
||||
public int Page { get; set; } = 1;
|
||||
public int Count { get; set; } = 20;
|
||||
public int Total { get; set; }
|
||||
|
||||
@@ -17,6 +17,12 @@ public class SimApiOptions
|
||||
/// </summary>
|
||||
public bool EnableSimApiAuth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启用Cache功能
|
||||
/// </summary>
|
||||
public bool EnableSimApiCache { get; set; } = true;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 启用SimApi网关授权, 基于上层网关透传的身份令牌验证
|
||||
/// </summary>
|
||||
@@ -56,6 +62,12 @@ public class SimApiOptions
|
||||
/// </summary>
|
||||
public bool EnableSimApiResponseFilter { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 启用请求日志中间件
|
||||
/// default: false
|
||||
/// </summary>
|
||||
public bool EnableRequestLog { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开启ForwardHeaders,开启后可以透传负载均衡的Headers
|
||||
/// default: true
|
||||
@@ -104,6 +116,8 @@ public class SimApiOptions
|
||||
|
||||
public SimApiRouteOptions SimApiRouteOptions { get; set; } = new();
|
||||
|
||||
public SimApiRequestLogOptions SimApiRequestLogOptions { get; set; } = new();
|
||||
|
||||
public void ConfigureSimApiRoute(Action<SimApiRouteOptions>? options = null)
|
||||
{
|
||||
options?.Invoke(SimApiRouteOptions);
|
||||
@@ -143,4 +157,9 @@ public class SimApiOptions
|
||||
{
|
||||
options?.Invoke(SimApiAuthCenterOptions);
|
||||
}
|
||||
|
||||
public void ConfigureSimApiRequestLog(Action<SimApiRequestLogOptions>? options = null)
|
||||
{
|
||||
options?.Invoke(SimApiRequestLogOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace SimApi.Configurations;
|
||||
|
||||
public class SimApiRequestLogOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否打印完整的请求Header
|
||||
/// </summary>
|
||||
public bool ShowFullHeader { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否打印完整的响应体
|
||||
/// </summary>
|
||||
public bool ShowFullResponse { get; set; }
|
||||
}
|
||||
+82
-17
@@ -1,19 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SimApi.Communications;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace SimApi.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// 认证助手
|
||||
/// 认证助手(支持 Redis 和 InMemory 两种模式)
|
||||
/// </summary>
|
||||
public class SimApiAuth(IDistributedCache cache, IConnectionMultiplexer redis)
|
||||
public class SimApiAuth
|
||||
{
|
||||
private const string TokenCacheKey = "SimApi:Auth:Token:{token}";
|
||||
private const string TokenSetCacheKey = "SimApi:Auth:User:{userId}";
|
||||
private readonly IDatabase _redisDb = redis.GetDatabase();
|
||||
|
||||
private readonly IDistributedCache _cache;
|
||||
private readonly IDatabase? _redisDb;
|
||||
|
||||
// InMemory 模式:用户 → Token集合
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, byte>> _userTokens = new();
|
||||
|
||||
public SimApiAuth(IDistributedCache cache, IServiceProvider sp)
|
||||
{
|
||||
_cache = cache;
|
||||
_redisDb = sp.GetService<IConnectionMultiplexer>()?.GetDatabase();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 登录信息
|
||||
@@ -28,13 +41,20 @@ public class SimApiAuth(IDistributedCache cache, IConnectionMultiplexer redis)
|
||||
token ??= Guid.NewGuid().ToString();
|
||||
var cacheKey = TokenCacheKey.Replace("{token}", token);
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", loginItem.Id);
|
||||
_redisDb.SetAdd(setCacheKey, token);
|
||||
cache.SetString(cacheKey, SimApiUtil.Json(loginItem),
|
||||
new DistributedCacheEntryOptions
|
||||
|
||||
_cache.SetString(cacheKey, SimApiUtil.Json(loginItem),
|
||||
new DistributedCacheEntryOptions { SlidingExpiration = expireTime });
|
||||
|
||||
if (_redisDb != null)
|
||||
{
|
||||
SlidingExpiration = expireTime
|
||||
});
|
||||
_redisDb.SetAdd(setCacheKey, token);
|
||||
_redisDb.KeyExpire(setCacheKey, expireTime.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
var tokens = _userTokens.GetOrAdd(loginItem.Id, _ => new ConcurrentDictionary<string, byte>());
|
||||
tokens.TryAdd(token, 0);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
@@ -48,10 +68,15 @@ public class SimApiAuth(IDistributedCache cache, IConnectionMultiplexer redis)
|
||||
public string Update(SimApiLoginItem loginItem, string token)
|
||||
{
|
||||
var cacheKey = TokenCacheKey.Replace("{token}", token);
|
||||
cache.SetString(cacheKey, SimApiUtil.Json(loginItem));
|
||||
_cache.SetString(cacheKey, SimApiUtil.Json(loginItem));
|
||||
|
||||
if (_redisDb != null)
|
||||
{
|
||||
var ttl = _redisDb.KeyTimeToLive(cacheKey);
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", loginItem.Id);
|
||||
_redisDb.KeyExpire(setCacheKey, ttl);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -64,9 +89,10 @@ public class SimApiAuth(IDistributedCache cache, IConnectionMultiplexer redis)
|
||||
public SimApiLoginItem? GetLogin(string 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;
|
||||
if (resp != null)
|
||||
|
||||
if (resp != null && _redisDb != null)
|
||||
{
|
||||
var ttl = _redisDb.KeyTimeToLive(cacheKey);
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", resp.Id);
|
||||
@@ -82,6 +108,8 @@ public class SimApiAuth(IDistributedCache cache, IConnectionMultiplexer redis)
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public SimApiLoginItem[] GetAllLogins(string userId)
|
||||
{
|
||||
if (_redisDb != null)
|
||||
{
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", userId);
|
||||
var allLogins = _redisDb.SetMembers(setCacheKey).ToStringArray();
|
||||
@@ -92,24 +120,40 @@ public class SimApiAuth(IDistributedCache cache, IConnectionMultiplexer redis)
|
||||
{
|
||||
var item = GetLogin(login);
|
||||
if (item != null)
|
||||
{
|
||||
resp.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
_redisDb.SetRemove(setCacheKey, login);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_userTokens.TryGetValue(userId, out var tokens))
|
||||
return [];
|
||||
|
||||
var resp = new List<SimApiLoginItem>();
|
||||
foreach (var token in tokens.Keys)
|
||||
{
|
||||
var item = GetLogin(token);
|
||||
if (item != null)
|
||||
resp.Add(item);
|
||||
else
|
||||
tokens.TryRemove(token, out _);
|
||||
}
|
||||
|
||||
return resp.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 退出所有登录
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
public void LogoutAll(string userId)
|
||||
{
|
||||
if (_redisDb != null)
|
||||
{
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", userId);
|
||||
var allLogins = _redisDb.SetMembers(setCacheKey).ToStringArray();
|
||||
@@ -118,12 +162,24 @@ public class SimApiAuth(IDistributedCache cache, IConnectionMultiplexer redis)
|
||||
if (login != null)
|
||||
{
|
||||
var cacheKey = TokenCacheKey.Replace("{token}", login);
|
||||
cache.Remove(cacheKey);
|
||||
_cache.Remove(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
_redisDb.KeyDelete(setCacheKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_userTokens.TryRemove(userId, out var tokens))
|
||||
{
|
||||
foreach (var token in tokens.Keys)
|
||||
{
|
||||
var cacheKey = TokenCacheKey.Replace("{token}", token);
|
||||
_cache.Remove(cacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 退出登陆
|
||||
@@ -132,13 +188,22 @@ public class SimApiAuth(IDistributedCache cache, IConnectionMultiplexer redis)
|
||||
public void Logout(string token)
|
||||
{
|
||||
var item = GetLogin(token);
|
||||
|
||||
if (_redisDb != null)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", item.Id);
|
||||
_redisDb.SetRemove(setCacheKey, token);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (item != null && _userTokens.TryGetValue(item.Id, out var tokens))
|
||||
tokens.TryRemove(token, out _);
|
||||
}
|
||||
|
||||
var cacheKey = TokenCacheKey.Replace("{token}", token);
|
||||
cache.Remove(cacheKey);
|
||||
_cache.Remove(cacheKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SimApi.Configurations;
|
||||
|
||||
namespace SimApi.Middlewares;
|
||||
|
||||
/// <summary>
|
||||
/// 请求日志中间件
|
||||
/// </summary>
|
||||
public class SimApiRequestLogMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<SimApiRequestLogMiddleware> log,
|
||||
SimApiRequestLogOptions options)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var fullUrl =
|
||||
$"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
|
||||
|
||||
var logMessage = new StringBuilder();
|
||||
logMessage.AppendLine($"[{context.Request.Method}] {fullUrl}");
|
||||
|
||||
if (options.ShowFullHeader)
|
||||
{
|
||||
logMessage.AppendLine("*( RequestHeaders [Full] ) =>");
|
||||
var headersDict = new Dictionary<string, string>();
|
||||
foreach (var header in context.Request.Headers)
|
||||
{
|
||||
headersDict[header.Key] = header.Value.ToString();
|
||||
}
|
||||
|
||||
logMessage.AppendLine(JsonSerializer.Serialize(headersDict));
|
||||
}
|
||||
else
|
||||
{
|
||||
logMessage.AppendLine("*( RequestHeaders ) =>");
|
||||
var token = context.Request.Headers["Token"].FirstOrDefault() ?? "";
|
||||
var queryId = context.Request.Headers["Query-Id"].FirstOrDefault() ?? "";
|
||||
logMessage.AppendLine($"Token: {token} QueryId: {queryId}");
|
||||
}
|
||||
|
||||
context.Request.EnableBuffering();
|
||||
var requestBodyText = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
||||
context.Request.Body.Seek(0, SeekOrigin.Begin);
|
||||
logMessage.AppendLine("*( RequestBody ) =>");
|
||||
logMessage.AppendLine(requestBodyText);
|
||||
|
||||
var originalBodyStream = context.Response.Body;
|
||||
using var responseBody = new MemoryStream();
|
||||
context.Response.Body = responseBody;
|
||||
|
||||
ExceptionDispatchInfo? edi = null;
|
||||
|
||||
try
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
edi = ExceptionDispatchInfo.Capture(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
sw.Stop();
|
||||
|
||||
responseBody.Seek(0, SeekOrigin.Begin);
|
||||
var responseText = await new StreamReader(responseBody).ReadToEndAsync();
|
||||
|
||||
logMessage.AppendLine($"*( Response [{context.Response.StatusCode}] ) =>");
|
||||
if (options.ShowFullResponse)
|
||||
{
|
||||
logMessage.Append(responseText);
|
||||
}
|
||||
else
|
||||
{
|
||||
var truncated = responseText.Length > 200 ? responseText[..200] : responseText;
|
||||
logMessage.Append(truncated);
|
||||
}
|
||||
|
||||
if (edi != null)
|
||||
{
|
||||
logMessage.Append(Environment.NewLine);
|
||||
logMessage.Append($"Exception: {edi.SourceException}");
|
||||
}
|
||||
|
||||
responseBody.Seek(0, SeekOrigin.Begin);
|
||||
await responseBody.CopyToAsync(originalBodyStream);
|
||||
context.Response.Body = originalBodyStream;
|
||||
|
||||
log.LogInformation(logMessage.ToString());
|
||||
}
|
||||
|
||||
edi?.Throw();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ ASP.NET Core API 基础框架库,提供统一异常拦截、响应封装、Tok
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddSimApi(options =>
|
||||
{
|
||||
// RedisConfiguration 可选:配置则使用 Redis,不配则自动使用 InMemory
|
||||
options.RedisConfiguration = "localhost:6379";
|
||||
options.EnableSimApiAuth = true;
|
||||
options.EnableSimApiDoc = true;
|
||||
@@ -191,6 +192,26 @@ public class SimApiLoginItem {
|
||||
|
||||
**Token 传参**: Header `Token: <value>`
|
||||
|
||||
### 存储模式
|
||||
|
||||
`SimApiAuth` 内部自动判断,无需手动配置:
|
||||
|
||||
| 条件 | 存储后端 | 用户↔Token 映射 |
|
||||
|------|---------|-----------------|
|
||||
| 配置了 `RedisConfiguration` | Redis(`IDistributedCache` + Set) | Redis Set |
|
||||
| 未配置 `RedisConfiguration` | InMemory(`DistributedMemoryCache`) | `ConcurrentDictionary` |
|
||||
|
||||
- **Redis 模式**:支持多实例共享,Token 持久化,适合生产环境
|
||||
- **InMemory 模式**:零配置即可启用 `EnableSimApiAuth`,适合开发/测试/单实例场景。注意重启后所有登录态丢失
|
||||
|
||||
```csharp
|
||||
// 不配 Redis 也能用 Auth
|
||||
builder.Services.AddSimApi(options =>
|
||||
{
|
||||
options.EnableSimApiAuth = true; // 自动使用 InMemory
|
||||
});
|
||||
```
|
||||
|
||||
### 认证后处理 Hook — ISimApiAuthChecker
|
||||
|
||||
```csharp
|
||||
@@ -406,9 +427,11 @@ IMinioClient Client { get; } // 底层 MinIO 客户端
|
||||
|
||||
---
|
||||
|
||||
## 8. Redis 缓存 — SimApiCache
|
||||
## 8. 缓存 — SimApiCache
|
||||
|
||||
依赖 `RedisConfiguration`,Key 自动加前缀 `SimApi:Cache:`。
|
||||
通过 `EnableSimApiCache`(默认 `true`)控制。Key 自动加前缀 `SimApi:Cache:`。
|
||||
|
||||
存储后端与 `SimApiAuth` 一致:配了 `RedisConfiguration` 就用 Redis,否则用 InMemory。
|
||||
|
||||
```csharp
|
||||
void Set(string key, object value, DistributedCacheEntryOptions? options = null);
|
||||
@@ -617,6 +640,7 @@ builder.Services.AddSimApi(options =>
|
||||
|
||||
// 功能开关
|
||||
options.EnableSimApiAuth = false; // Token 认证
|
||||
options.EnableSimApiCache = true; // 缓存(Redis 或 InMemory)
|
||||
options.EnableSimApiAuthGate = false; // Auth Center 网关鉴权
|
||||
options.EnableSimApiDoc = false; // Swagger 文档
|
||||
options.EnableSimApiStorage = false; // S3 存储
|
||||
|
||||
@@ -36,11 +36,20 @@ public static class SimApiExtensions
|
||||
{
|
||||
var simApiOptions = new SimApiOptions();
|
||||
options?.Invoke(simApiOptions);
|
||||
|
||||
if (simApiOptions.RedisConfiguration != null)
|
||||
{
|
||||
builder.AddStackExchangeRedisCache(x => x.Configuration = simApiOptions.RedisConfiguration);
|
||||
builder.AddSingleton<IConnectionMultiplexer>(_ =>
|
||||
ConnectionMultiplexer.Connect(simApiOptions.RedisConfiguration));
|
||||
}
|
||||
else if (simApiOptions.EnableSimApiAuth || simApiOptions.EnableSimApiCache)
|
||||
{
|
||||
builder.AddDistributedMemoryCache();
|
||||
}
|
||||
|
||||
if (simApiOptions.EnableSimApiCache)
|
||||
{
|
||||
builder.AddSingleton<SimApiCache>();
|
||||
}
|
||||
|
||||
@@ -209,6 +218,7 @@ public static class SimApiExtensions
|
||||
x.OperationFilter<SimApiSignOperationFilter>();
|
||||
x.OperationFilter<AesBodyOperationFilter>();
|
||||
x.SchemaFilter<GlobalDynamicObjectSchemaFilter>();
|
||||
x.SchemaFilter<DictionarySchemaFilter>();
|
||||
x.DocumentFilter<RemoveEmptyTagsFilter>();
|
||||
if (simApiOptions.EnableSimApiAuth)
|
||||
{
|
||||
@@ -339,6 +349,7 @@ public static class SimApiExtensions
|
||||
}
|
||||
}
|
||||
|
||||
builder.AddSingleton(simApiOptions.SimApiRequestLogOptions);
|
||||
builder.AddSingleton(simApiOptions);
|
||||
return builder;
|
||||
}
|
||||
@@ -358,6 +369,11 @@ public static class SimApiExtensions
|
||||
logger.LogInformation("开始配置 RedisCache ...");
|
||||
}
|
||||
|
||||
if (options.EnableSimApiCache)
|
||||
{
|
||||
logger.LogInformation("开始配置SimApiCache...");
|
||||
}
|
||||
|
||||
//请求一下检测存储错误
|
||||
if (options.EnableSimApiStorage)
|
||||
{
|
||||
@@ -497,6 +513,12 @@ public static class SimApiExtensions
|
||||
});
|
||||
}
|
||||
|
||||
if (options.EnableRequestLog)
|
||||
{
|
||||
logger.LogInformation("开始配置SimApiRequestLog...");
|
||||
builder.UseMiddleware<SimApiRequestLogMiddleware>();
|
||||
}
|
||||
|
||||
if (options.EnableSimApiException)
|
||||
{
|
||||
logger.LogInformation("开始配置SimApiException...");
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace SimApi.SwaggerFilters;
|
||||
|
||||
public class DictionarySchemaFilter : ISchemaFilter
|
||||
{
|
||||
public void Apply(IOpenApiSchema schema, SchemaFilterContext context)
|
||||
{
|
||||
if (!context.Type.IsGenericType || context.Type.GetGenericTypeDefinition() != typeof(Dictionary<,>))
|
||||
return;
|
||||
|
||||
if (schema is not OpenApiSchema concrete) return;
|
||||
|
||||
var valueType = context.Type.GetGenericArguments()[1];
|
||||
concrete.Type = JsonSchemaType.Object;
|
||||
concrete.AdditionalPropertiesAllowed = true;
|
||||
concrete.AdditionalProperties = context.SchemaGenerator.GenerateSchema(valueType, context.SchemaRepository);
|
||||
concrete.Properties?.Clear();
|
||||
concrete.Example = null;
|
||||
concrete.Examples?.Clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user