Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4f051cb61 | ||
|
|
7b34991ea0 | ||
|
|
1623343748 | ||
|
|
d8a32f3f07 | ||
|
|
e17b9d9eed | ||
|
|
80a05ea20b | ||
|
|
33f7cff11c | ||
|
|
c7a4395000 | ||
|
|
bf98792f96 | ||
|
|
8c5081d442 | ||
|
|
2ed4605a2c | ||
|
|
ceeea2c451 | ||
|
|
464878edce | ||
|
|
aaba8b8b73 | ||
|
|
6e2ae11c54 | ||
|
|
d0a5e996c9 | ||
|
|
3b405079c1 | ||
|
|
611808787e | ||
|
|
8ee297e819 | ||
|
|
bc4195e984 | ||
|
|
4a0caa1079 | ||
|
|
7a2ff896b2 |
@@ -434,3 +434,7 @@ MigrationBackup/
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
|
||||
# BitFun snapshot data - auto managed
|
||||
.bitfun/
|
||||
/.bitfun/search/flashgrep-index/
|
||||
|
||||
@@ -1,39 +1,105 @@
|
||||
using System;
|
||||
using System;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace SimApi.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// 快捷自定义接口文档类
|
||||
/// 快捷自定义接口文档类(所有参数均可省略, 支持命名参数)。
|
||||
/// 对应仓颉版 SimApiDoc:
|
||||
/// tags → 接口标签(逗号分隔, 如 "认证,用户")
|
||||
/// name → API 名称(映射到 Summary, 作为文档接口标题)
|
||||
/// description → API 详细描述
|
||||
/// groupNames → 所属文档组(逗号分隔, 如 "api,admin"; "*" 表示所有文档; null/空 → 仅默认 "api" 文档)
|
||||
/// ignore → true 时不出现在任何文档中(路由不受影响)
|
||||
/// 写法示例:
|
||||
/// [SimApiDoc("认证", "登录")] 位置参数(保持旧版兼容)
|
||||
/// [SimApiDoc(tags: "认证", name: "登录", description: "...")]
|
||||
/// [SimApiDoc(groupNames: "api,admin")] 仅指定文档组
|
||||
/// [SimApiDoc(GroupNames = "*", Ignore = false)] 属性命名参数
|
||||
/// [SimApiDoc] 全部默认(可标在类上, 仅用 Ignore/GroupNames 等)
|
||||
/// 属性命名参数优先于构造命名参数。
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public class SimApiDocAttribute : SwaggerOperationAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 定义接口说明
|
||||
/// 出现在所有文档组中的通配符
|
||||
/// </summary>
|
||||
/// <param name="tags">接口分组列表</param>
|
||||
public const string AllGroups = "*";
|
||||
|
||||
/// <summary>
|
||||
/// 接口所属的文档组(逗号分隔, 如 "api,admin"); "*" 表示所有文档; null/空 表示未分组(仅默认 "api" 文档)
|
||||
/// </summary>
|
||||
public string? GroupNames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 为 true 时该接口不出现在任何文档中(不影响路由)
|
||||
/// </summary>
|
||||
public bool Ignore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 定义接口说明(全部可选)
|
||||
/// </summary>
|
||||
/// <param name="tags">接口标签, 逗号分隔, 如 "认证,用户"</param>
|
||||
/// <param name="name">接口名称</param>
|
||||
/// <param name="description">接口描述</param>
|
||||
public SimApiDocAttribute(string[] tags, string name, string? description = null)
|
||||
/// <param name="groupNames">所属文档组, 逗号分隔; "*" 表示所有文档</param>
|
||||
/// <param name="ignore">true 时不出现在任何文档</param>
|
||||
public SimApiDocAttribute(string? tags = null, string? name = null, string? description = null,
|
||||
string? groupNames = null, bool ignore = false)
|
||||
{
|
||||
Tags = tags;
|
||||
Summary = name;
|
||||
if (description != null)
|
||||
{
|
||||
Description = description;
|
||||
}
|
||||
// Consumes = new[] {"application/json"};
|
||||
// Produces = new[] {"application/json"};
|
||||
Apply(tags, name, description, groupNames, ignore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定义接口说明
|
||||
/// 定义接口说明(标签以数组传入)
|
||||
/// </summary>
|
||||
/// <param name="tag">接口分组</param>
|
||||
/// <param name="tags">接口标签列表</param>
|
||||
/// <param name="name">接口名称</param>
|
||||
/// <param name="description">接口描述</param>
|
||||
public SimApiDocAttribute(string tag, string name, string? description = null) : this([tag], name, description)
|
||||
/// <param name="groupNames">所属文档组, 逗号分隔; "*" 表示所有文档</param>
|
||||
/// <param name="ignore">true 时不出现在任何文档</param>
|
||||
public SimApiDocAttribute(string[] tags, string? name = null, string? description = null,
|
||||
string? groupNames = null, bool ignore = false)
|
||||
{
|
||||
Apply(tags, name, description, groupNames, ignore);
|
||||
}
|
||||
}
|
||||
|
||||
private void Apply(string? tags, string? name, string? description,
|
||||
string? groupNames, bool ignore)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tags))
|
||||
{
|
||||
Tags = tags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
ApplyCore(name, description, groupNames, ignore);
|
||||
}
|
||||
|
||||
private void Apply(string[] tags, string? name, string? description,
|
||||
string? groupNames, bool ignore)
|
||||
{
|
||||
if (tags is { Length: > 0 })
|
||||
{
|
||||
Tags = tags;
|
||||
}
|
||||
|
||||
ApplyCore(name, description, groupNames, ignore);
|
||||
}
|
||||
|
||||
private void ApplyCore(string? name, string? description, string? groupNames, bool ignore)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
Summary = name;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(description))
|
||||
{
|
||||
Description = description;
|
||||
}
|
||||
|
||||
GroupNames = groupNames;
|
||||
Ignore = ignore;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using SimApi.Communications;
|
||||
using static SimApi.Helpers.SimApiError;
|
||||
|
||||
namespace SimApi.AuthSDK;
|
||||
|
||||
public class SimApiAuthCenter(SimApiAuthClient simapi)
|
||||
{
|
||||
public SimApiAuthClient Client { get; } = simapi;
|
||||
|
||||
#region 公共
|
||||
|
||||
/// <summary>
|
||||
/// 委托AuthCenter进行应用签名验证
|
||||
/// </summary>
|
||||
/// <param name="appId"></param>
|
||||
/// <param name="timestamp"></param>
|
||||
/// <param name="nonce"></param>
|
||||
/// <param name="sign"></param>
|
||||
public void VerifySign(string appId, string timestamp, string nonce, string sign)
|
||||
{
|
||||
var http = new HttpClient();
|
||||
var url = $"{Client.Server}/api/auth/sign/verify?appId={appId}×tamp={timestamp}&nonce={nonce}&sign={sign}";
|
||||
var result = http.PostAsJsonAsync(url, new { }).Result;
|
||||
var resp = result.Content.ReadFromJsonAsync<SimApiBaseResponse>().Result;
|
||||
ErrorWhenNull(resp, 400, "签名验证失败");
|
||||
ErrorWhenFalse(resp.Code == 200, 400, "签名验证失败");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region 群组相关
|
||||
|
||||
/// <summary>
|
||||
/// 根据profileId获取群组列表
|
||||
/// </summary>
|
||||
/// <param name="profileId"></param>
|
||||
/// <returns></returns>
|
||||
public SimApiAuthCenterDto.GroupRelatedItem[]? GroupRelated(string profileId)
|
||||
{
|
||||
return Client.SignQuery<SimApiAuthCenterDto.GroupRelatedItem[]>("/api/auth/group/related", new { profileId });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据关键字搜索群组,输入群组ID精准搜索
|
||||
/// </summary>
|
||||
/// <param name="keyword"></param>
|
||||
/// <param name="skip"></param>
|
||||
/// <param name="take"></param>
|
||||
/// <returns></returns>
|
||||
public SimApiAuthCenterDto.AppAndProfileItem[]? GroupSearch(string keyword, int skip = 0, int take = 20)
|
||||
{
|
||||
return Client.SignQuery<SimApiAuthCenterDto.AppAndProfileItem[]>("/api/auth/group/search",
|
||||
new { keyword, skip, take });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用组ID以及组内成员,管理员profile获取组的详细树结构
|
||||
/// </summary>
|
||||
/// <param name="profileId"></param>
|
||||
/// <param name="groupId"></param>
|
||||
/// <returns></returns>
|
||||
public SimApiAuthCenterDto.GroupDetailTreeNode? GroupDetail(string groupId, string profileId)
|
||||
{
|
||||
return Client.SignQuery<SimApiAuthCenterDto.GroupDetailTreeNode>("/api/auth/group/detail",
|
||||
new { profileId, groupId });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取profile在本组的所有子组
|
||||
/// </summary>
|
||||
/// <param name="groupId"></param>
|
||||
/// <param name="profileId"></param>
|
||||
/// <returns></returns>
|
||||
public string[]? GroupRelatedIndex(string groupId, string profileId)
|
||||
{
|
||||
return Client.SignQuery<string[]>("/api/auth/internal/group/related-group-ids", new { groupId, profileId });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Profile相关
|
||||
|
||||
/// <summary>
|
||||
/// 根据关键字,搜索用户Profile,参数支持精准ID,用户手机号,用户邮箱,Profile名称模糊搜索
|
||||
/// </summary>
|
||||
/// <param name="keyword"></param>
|
||||
/// <param name="skip"></param>
|
||||
/// <param name="take"></param>
|
||||
/// <returns></returns>
|
||||
public SimApiAuthCenterDto.AppAndProfileItem[]? ProfileSearch(string keyword, int skip = 0, int take = 20)
|
||||
{
|
||||
return Client.SignQuery<SimApiAuthCenterDto.AppAndProfileItem[]>("/api/auth/profile/search",
|
||||
new { keyword, skip, take });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过id,可以批量获取用户的基本信息
|
||||
/// </summary>
|
||||
/// <param name="ids"></param>
|
||||
/// <returns></returns>
|
||||
public SimApiAuthCenterDto.AppAndProfileItem[]? ProfileList(string[] ids)
|
||||
{
|
||||
return Client.SignQuery<SimApiAuthCenterDto.AppAndProfileItem[]>("/api/auth/profile/list", new { ids });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AuthGate内部应用专用 - 注意: 只有内部应用可以调用
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否为App的拥有者
|
||||
/// </summary>
|
||||
/// <param name="profileId"></param>
|
||||
/// <param name="applicationId"></param>
|
||||
/// <returns></returns>
|
||||
public bool CheckIsAppOwner(string profileId, string applicationId)
|
||||
{
|
||||
return Client.SignQuery<bool>("/api/auth/internal/app/check-owner", new
|
||||
{
|
||||
ProfileId = profileId,
|
||||
AppId = applicationId,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取应用列表,根据用户profileId 和 提供的appIds
|
||||
/// </summary>
|
||||
/// <param name="profileId"></param>
|
||||
/// <param name="appIds"></param>
|
||||
/// <returns></returns>
|
||||
public SimApiAuthCenterDto.AppAndProfileItem[]? GetAppList(string profileId, IEnumerable<string> appIds)
|
||||
{
|
||||
return Client.SignQuery<SimApiAuthCenterDto.AppAndProfileItem[]>("/api/auth/internal/app/related", new
|
||||
{
|
||||
ProfileId = profileId,
|
||||
AllowedAppIds = appIds,
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 系统登录
|
||||
|
||||
/// <summary>
|
||||
/// 获取登录授权CODE
|
||||
/// </summary>
|
||||
/// <param name="scene"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="backUrl"></param>
|
||||
/// <returns></returns>
|
||||
public SimApiAuthCenterDto.GetCodeResponse GetLoginCode(string? scene = null,
|
||||
Dictionary<string, object>? data = null,
|
||||
string? backUrl = null)
|
||||
{
|
||||
var code = Client.SignQuery<string>("/api/auth/login/code",
|
||||
new { scene, data, backUrl });
|
||||
return new SimApiAuthCenterDto.GetCodeResponse()
|
||||
{
|
||||
Code = code!,
|
||||
Server = Client.Server,
|
||||
FullUrl = $"{Client.Server}/auth?code={code}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用code获取登录信息
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="scene"></param>
|
||||
/// <returns></returns>
|
||||
public SimApiAuthCenterDto.LoginInfoResponse GetLoginInfo(string code, string? scene = null)
|
||||
{
|
||||
var resp = Client.SignQuery<SimApiAuthCenterDto.LoginInfoResponse>("/api/auth/login/get", new { code });
|
||||
ErrorWhenNull(resp, 400232, "登录信息获取失败");
|
||||
ErrorWhen(resp.Scene != scene, 403003, "登录场景不匹配");
|
||||
return resp;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 安全验证
|
||||
|
||||
/// <summary>
|
||||
/// 获取安全验证代码
|
||||
/// </summary>
|
||||
/// <param name="scene"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="backUrl"></param>
|
||||
/// <returns></returns>
|
||||
public SimApiAuthCenterDto.GetCodeResponse GetConfirmCode(string scene, string userId,
|
||||
Dictionary<string, object>? data = null,
|
||||
string? backUrl = null)
|
||||
{
|
||||
var code = Client.SignQuery<string>("/api/auth/confirm/code",
|
||||
new { scene, data, backUrl, profileId = userId });
|
||||
return new SimApiAuthCenterDto.GetCodeResponse()
|
||||
{
|
||||
Code = code!,
|
||||
Server = Client.Server,
|
||||
FullUrl = $"{Client.Server}/confirm?code={code}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用安全验证code 获取验证结果
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="scene"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public SimApiAuthCenterDto.ConfirmResponse Confirm(string code, string scene, string? userId = null)
|
||||
{
|
||||
var resp = Client.SignQuery<SimApiAuthCenterDto.ConfirmResponse>("/api/auth/confirm/get", new { code });
|
||||
ErrorWhenNull(resp, 403001, "安全确认码无效");
|
||||
ErrorWhen(resp.ProfileId != userId, 403002, "安全确认身份不匹配");
|
||||
ErrorWhen(resp.Scene != scene, 403003, "安全确认场景不匹配");
|
||||
return resp;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SimApi.AuthSDK;
|
||||
|
||||
public class SimApiAuthCenterDto
|
||||
{
|
||||
public class AppAndProfileItem
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string? Image { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
public class ConfirmResponse
|
||||
{
|
||||
public required string ApplicationId { get; set; }
|
||||
public required string ProfileId { get; set; }
|
||||
public string? Scene { get; set; }
|
||||
public Dictionary<string, object>? Data { get; set; }
|
||||
}
|
||||
|
||||
public class LoginInfoResponse
|
||||
{
|
||||
public string? Scene { get; set; }
|
||||
public Dictionary<string, object>? Data { get; set; }
|
||||
public required string ProfileId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string? Image { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
public class GetCodeResponse
|
||||
{
|
||||
public required string Code { get; set; }
|
||||
public required string Server { get; set; }
|
||||
public required string FullUrl { get; set; }
|
||||
}
|
||||
|
||||
public class GroupRelatedItem
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string? Image { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool IsOwner { get; set; }
|
||||
public bool IsAdmin { get; set; }
|
||||
public bool IsMember { get; set; }
|
||||
}
|
||||
|
||||
public class GroupDetailTreeNode
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string? Image { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public int Sort { get; set; }
|
||||
public List<GroupDetailTreeNode> Children { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SimApi.Communications;
|
||||
using SimApi.Configurations;
|
||||
using SimApi.Helpers;
|
||||
|
||||
namespace SimApi.Middlewares;
|
||||
namespace SimApi.AuthSDK;
|
||||
|
||||
public class SimApiGateAuthMiddleware(RequestDelegate next, ILogger<SimApiGateAuthMiddleware> logger)
|
||||
public class SimApiAuthCenterMiddleware(RequestDelegate next, ILogger<SimApiAuthCenterMiddleware> logger)
|
||||
{
|
||||
public Task Invoke(HttpContext httpContext, SimApiOptions simApiOptions)
|
||||
{
|
||||
@@ -17,7 +16,7 @@ public class SimApiGateAuthMiddleware(RequestDelegate next, ILogger<SimApiGateAu
|
||||
httpContext.Request.Headers.TryGetValue("X-SimApi-Gate-Sign", out var sign))
|
||||
{
|
||||
var signStr =
|
||||
$"appId={simApiOptions.SimApiGateAuthOptions.AppId}&auth={auth}&time={time}&appKey={simApiOptions.SimApiGateAuthOptions.AppKey}";
|
||||
$"appId={simApiOptions.SimApiAuthCenterOptions.AppId}&auth={auth}&time={time}&appKey={simApiOptions.SimApiAuthCenterOptions.AppKey}";
|
||||
logger.LogDebug($"签名字符串 => {signStr}");
|
||||
if (SimApiUtil.Md5(signStr) == sign && !string.IsNullOrEmpty(auth))
|
||||
{
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SimApi.Configurations;
|
||||
using SimApi.Helpers;
|
||||
|
||||
namespace SimApi.AuthSDK;
|
||||
|
||||
public class SimApiAuthClient(SimApiOptions apiOptions, ILogger<SimApiAuthClient> logger)
|
||||
: SimApiHttpClient(apiOptions, logger)
|
||||
{
|
||||
public override string Server { get; init; } = apiOptions.SimApiAuthCenterOptions.Server ?? string.Empty;
|
||||
public override string AppId { get; init; } = apiOptions.SimApiAuthCenterOptions.AppId ?? string.Empty;
|
||||
public override string AppKey { get; init; } = apiOptions.SimApiAuthCenterOptions.AppKey ?? string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static SimApi.Helpers.SimApiError;
|
||||
|
||||
namespace SimApi.AuthSDK;
|
||||
|
||||
public class SimApiAuthIam(SimApiAuthClient simapi, ILogger<SimApiAuthIam> logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// 向Iam注册权限
|
||||
/// </summary>
|
||||
/// <param name="permissions"></param>
|
||||
public void RegisterPermissions(SimApiAuthIamDto.PermissionItem[] permissions)
|
||||
{
|
||||
var log = $"检测到 {permissions.Length} 个权限接口,正在注册..: ";
|
||||
foreach (var permission in permissions)
|
||||
{
|
||||
log += $"\n |- {permission.Identifier} => [{permission.Group}]{permission.Name} ({permission.Description})";
|
||||
}
|
||||
|
||||
logger.LogInformation(log);
|
||||
simapi.SignQuery<string>("/api/iam/permission/register", new { permissions });
|
||||
logger.LogInformation("权限注册完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取拥有的权限标识数组
|
||||
/// </summary>
|
||||
/// <param name="profileId"></param>
|
||||
/// <param name="groupId"></param>
|
||||
/// <returns></returns>
|
||||
public string[] GetPermissionOwned(string profileId, string? groupId = null)
|
||||
{
|
||||
return simapi.SignQuery<string[]>("/api/iam/permission/owned", new { profileId, groupId }) ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测profileId是否有这个权限
|
||||
/// </summary>
|
||||
/// <param name="profileId"></param>
|
||||
/// <param name="permission"></param>
|
||||
/// <param name="groupId"></param>
|
||||
public void CheckPermission(string profileId, string permission, string? groupId = null)
|
||||
{
|
||||
var ok = simapi.SignQuery<bool>("/api/iam/permission/check", new { profileId, permission, groupId });
|
||||
ErrorWhen(!ok, 403, "没有该权限");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace SimApi.AuthSDK;
|
||||
|
||||
public class SimApiAuthIamDto
|
||||
{
|
||||
public class PermissionItem
|
||||
{
|
||||
public required string Identifier { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string Group { get; init; }
|
||||
public required string Description { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace SimApi.Configurations;
|
||||
|
||||
public class SimApiAuthCenterOptions
|
||||
{
|
||||
public string? Server { get; set; }
|
||||
public string? AppId { get; set; }
|
||||
public string? AppKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开启则使用内部网关透传的Middleware
|
||||
/// 注意: 只有内部应用需要开启这个,也就是api通过内部网关代理后
|
||||
/// </summary>
|
||||
public bool UseMiddleware { get; set; }
|
||||
public bool UseIam { get; set; }
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace SimApi.Configurations;
|
||||
/// <summary>
|
||||
/// 文档组配置
|
||||
/// </summary>
|
||||
public class SimApiDocGroupOption(string id, string name, string description = "")
|
||||
public class SimApiDocGroupOption(string id, string name, string description = "", bool isDefault = false)
|
||||
{
|
||||
/// <summary>
|
||||
/// 文档标识
|
||||
@@ -22,6 +22,11 @@ public class SimApiDocGroupOption(string id, string name, string description = "
|
||||
/// 文档描述
|
||||
/// </summary>
|
||||
public string Description { get; set; } = description!;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为默认文档组: 未标注分组的接口全部归入此文档组
|
||||
/// </summary>
|
||||
public bool IsDefault { get; set; } = isDefault;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -50,7 +55,7 @@ public class SimApiDocOptions
|
||||
/// </summary>
|
||||
public SimApiDocGroupOption[] ApiGroups { get; set; } =
|
||||
[
|
||||
new("api", "Api", "Api接口文档")
|
||||
new("api", "Api", "Api接口文档", isDefault: true)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
@@ -58,6 +63,12 @@ public class SimApiDocOptions
|
||||
/// </summary>
|
||||
public SimApiAuthOption ApiAuth { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 接口文档页面访问前缀, 默认 "docs"。
|
||||
/// 访问 /docs 即打开文档页面, JSON 地址为 /docs/{文档组Id}.json
|
||||
/// </summary>
|
||||
public string UrlPrefix { get; set; } = "docs";
|
||||
|
||||
/// <summary>
|
||||
/// 文档页面标题
|
||||
/// </summary>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace SimApi.Configurations;
|
||||
|
||||
public class SimApiGateAuthOptions
|
||||
{
|
||||
public string? AppId { get; set; }
|
||||
|
||||
public string? AppKey { get; set; }
|
||||
}
|
||||
@@ -1,11 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SimApi.Configurations;
|
||||
|
||||
public class SimApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Redis配置
|
||||
/// </summary>
|
||||
public string? RedisConfiguration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原样返回给前端的配置信息
|
||||
/// </summary>
|
||||
public Dictionary<string, object>? WebConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// WebConfig返回是否包含版本信息
|
||||
/// </summary>
|
||||
public bool WebConfigIncludeVersion { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用后台任务系统 *基于Hangfire
|
||||
/// </summary>
|
||||
@@ -18,14 +32,15 @@ public class SimApiOptions
|
||||
public bool EnableSimApiAuth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启用SimApi网关授权, 基于上层网关透传的身份令牌验证
|
||||
/// 启用Cache功能
|
||||
/// </summary>
|
||||
public bool EnableSimApiGateAuth { get; set; }
|
||||
public bool EnableSimApiCache { get; set; } = true;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用CoceSdk
|
||||
/// 启用SimApi网关授权, 基于上层网关透传的身份令牌验证
|
||||
/// </summary>
|
||||
public bool EnableCoceSdk { get; set; }
|
||||
public bool EnableSimApiAuthGate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开启S3兼容的存储系统。
|
||||
@@ -61,6 +76,11 @@ public class SimApiOptions
|
||||
/// </summary>
|
||||
public bool EnableSimApiResponseFilter { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 启用请求日志中间件
|
||||
/// default: false
|
||||
/// </summary>
|
||||
public bool EnableRequestLog { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开启ForwardHeaders,开启后可以透传负载均衡的Headers
|
||||
@@ -74,24 +94,17 @@ public class SimApiOptions
|
||||
/// </summary>
|
||||
public bool EnableLowerUrl { get; set; } = true;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 应用可以通过 /versions 显示出应用版本和SimApi包版本
|
||||
/// default: true
|
||||
/// </summary>
|
||||
public bool EnableVersionUrl { get; set; } = true;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 启用格式化的 Console Logger
|
||||
/// default: false
|
||||
/// </summary>
|
||||
public bool EnableLogger { get; set; } = true;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用SimApiHttpClient
|
||||
/// </summary>
|
||||
public bool EnableSimApiHttpClient { get; set; } = false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 配置Job
|
||||
/// </summary>
|
||||
@@ -109,12 +122,21 @@ public class SimApiOptions
|
||||
|
||||
public SimApiSynapseOptions SimApiSynapseOptions { get; set; } = new();
|
||||
|
||||
public SimApiGateAuthOptions SimApiGateAuthOptions { get; set; } = new();
|
||||
public SimApiAuthCenterOptions SimApiAuthCenterOptions { get; set; } = new();
|
||||
|
||||
public SimApiHttpClientOptions SimApiHttpClientOptions { get; set; } = new();
|
||||
|
||||
public SimApiExceptionOptions SimApiExceptionOptions { get; set; } = new();
|
||||
|
||||
public SimApiRouteOptions SimApiRouteOptions { get; set; } = new();
|
||||
|
||||
public SimApiRequestLogOptions SimApiRequestLogOptions { get; set; } = new();
|
||||
|
||||
public void ConfigureSimApiRoute(Action<SimApiRouteOptions>? options = null)
|
||||
{
|
||||
options?.Invoke(SimApiRouteOptions);
|
||||
}
|
||||
|
||||
public void ConfigureSimApiException(Action<SimApiExceptionOptions>? options = null)
|
||||
{
|
||||
options?.Invoke(SimApiExceptionOptions);
|
||||
@@ -145,8 +167,13 @@ public class SimApiOptions
|
||||
options?.Invoke(SimApiJobOptions);
|
||||
}
|
||||
|
||||
public void ConfigureSimApiGateAuth(Action<SimApiGateAuthOptions>? options = null)
|
||||
public void ConfigureSimApiAuthCenter(Action<SimApiAuthCenterOptions>? options = null)
|
||||
{
|
||||
options?.Invoke(SimApiGateAuthOptions);
|
||||
options?.Invoke(SimApiAuthCenterOptions);
|
||||
}
|
||||
|
||||
public void ConfigureSimApiRequestLog(Action<SimApiRequestLogOptions>? options = null)
|
||||
{
|
||||
options?.Invoke(SimApiRequestLogOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace SimApi.Configurations;
|
||||
|
||||
public class SimApiRequestLogOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否打印完整的请求Header
|
||||
/// </summary>
|
||||
public bool ShowFullHeader { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否打印完整的响应体
|
||||
/// </summary>
|
||||
public bool ShowFullResponse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 请求字段显示最长长度
|
||||
/// </summary>
|
||||
public int RequestStringLogLength { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 响应字段显示最长长度
|
||||
/// </summary>
|
||||
public int ResponseStringLogLength { get; set; } = 0;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SimApi.Configurations;
|
||||
|
||||
public class SimApiRouteOptions
|
||||
{
|
||||
public string? LogoutRoute = "/auth/logout";
|
||||
public string? UserInfoRoute = "/user/info";
|
||||
public string? WebConfigRoute = "/config";
|
||||
}
|
||||
@@ -5,15 +5,13 @@ using SimApi.Helpers;
|
||||
|
||||
namespace SimApi.Controllers;
|
||||
|
||||
using static SimApiError;
|
||||
|
||||
public class SimApiAuthController(SimApiAuth auth) : SimApiBaseController
|
||||
{
|
||||
/// <summary>
|
||||
/// 退出登陆
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost, SimApiDoc("认证", "退出登陆")]
|
||||
[HttpPost, SimApiDoc("认证", "退出登陆",groupNames: "*")]
|
||||
public void Logout()
|
||||
{
|
||||
if (Request.Headers.TryGetValue("Token", out var value))
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SimApi.Configurations;
|
||||
|
||||
namespace SimApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// SimApi 内置端点路由表中的一项。
|
||||
/// </summary>
|
||||
public sealed record SimApiBuiltInRoute(
|
||||
string RouteName,
|
||||
string Controller,
|
||||
string Action,
|
||||
string[] HttpMethods,
|
||||
string Path);
|
||||
|
||||
/// <summary>
|
||||
/// SimApi 内置端点路由表 —— 路径的唯一配置处。
|
||||
/// UseSimApi 用它注册约定路由(动态路由),
|
||||
/// SimApiBuiltInRoutesDescriptionProvider 用它生成文档条目。
|
||||
/// 某项路径为 null / 空时该端点既不注册路由、也不出现在文档中。
|
||||
/// </summary>
|
||||
public static class SimApiBuiltInRoutes
|
||||
{
|
||||
public static IEnumerable<SimApiBuiltInRoute> Get(SimApiRouteOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
if (!string.IsNullOrEmpty(options.UserInfoRoute))
|
||||
{
|
||||
yield return new SimApiBuiltInRoute(
|
||||
"UserInfo", "SimApiCommon", "UserInfo", ["POST"], options.UserInfoRoute);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(options.LogoutRoute))
|
||||
{
|
||||
yield return new SimApiBuiltInRoute(
|
||||
"Logout", "SimApiAuth", "Logout", ["POST"], options.LogoutRoute);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(options.WebConfigRoute))
|
||||
{
|
||||
// 控制器动作同时标注了 HttpGet/HttpPost
|
||||
yield return new SimApiBuiltInRoute(
|
||||
"WebConfig", "SimApiCommon", "WebConfig", ["GET", "POST"], options.WebConfigRoute);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SimApi.Configurations;
|
||||
|
||||
namespace SimApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 将 SimApiRouteOptions 动态配置的内置约定路由(UserInfo / Logout / WebConfig)
|
||||
/// 翻译成 ApiDescription, 使它们能像属性路由接口一样出现在 Swagger 文档中:
|
||||
/// 请求/响应 schema 由 Swashbuckle 自动分析, 方法上已有的 [SimApiDoc] 注解自动生效。
|
||||
/// 路径为 null / 空 的端点在此一并跳过(与路由注册保持一致)。
|
||||
/// </summary>
|
||||
public class SimApiBuiltInRoutesDescriptionProvider(
|
||||
IServiceProvider services,
|
||||
SimApiOptions options) : IApiDescriptionProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认 provider(DefaultApiDescriptionProvider)的 Order 为 -1000, 位于其后执行。
|
||||
/// </summary>
|
||||
public int Order => -900;
|
||||
|
||||
public void OnProvidersExecuting(ApiDescriptionProviderContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnProvidersExecuted(ApiDescriptionProviderContext context)
|
||||
{
|
||||
var modelMetadata = services.GetService<IModelMetadataProvider>();
|
||||
|
||||
foreach (var route in SimApiBuiltInRoutes.Get(options.SimApiRouteOptions))
|
||||
{
|
||||
var action = context.Actions.OfType<ControllerActionDescriptor>()
|
||||
.FirstOrDefault(a =>
|
||||
string.Equals(a.ControllerName, route.Controller, StringComparison.Ordinal) &&
|
||||
string.Equals(a.ActionName, route.Action, StringComparison.Ordinal));
|
||||
if (action is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 若该动作已被属性路由 / 其他 provider 收录, 跳过避免重复
|
||||
if (context.Results.Any(d => ReferenceEquals(d.ActionDescriptor, action)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var httpMethod in route.HttpMethods)
|
||||
{
|
||||
context.Results.Add(CreateDescription(action, route, httpMethod, modelMetadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ApiDescription CreateDescription(
|
||||
ControllerActionDescriptor action,
|
||||
SimApiBuiltInRoute route,
|
||||
string httpMethod,
|
||||
IModelMetadataProvider? modelMetadata)
|
||||
{
|
||||
var apiDescription = new ApiDescription
|
||||
{
|
||||
ActionDescriptor = action,
|
||||
HttpMethod = httpMethod,
|
||||
RelativePath = route.Path.TrimStart('/')
|
||||
};
|
||||
|
||||
// 输入: 按真实方法参数翻译(内置端点均为零参, 此处保证将来加参也可分析)
|
||||
foreach (var parameter in action.Parameters)
|
||||
{
|
||||
var parameterDescription = new ApiParameterDescription
|
||||
{
|
||||
Name = parameter.Name,
|
||||
Type = parameter.ParameterType,
|
||||
ParameterDescriptor = parameter,
|
||||
Source = parameter.BindingInfo?.BindingSource ?? BindingSource.Body,
|
||||
IsRequired = true
|
||||
};
|
||||
if (modelMetadata is not null)
|
||||
{
|
||||
parameterDescription.ModelMetadata = modelMetadata.GetMetadataForType(parameter.ParameterType);
|
||||
}
|
||||
|
||||
apiDescription.ParameterDescriptions.Add(parameterDescription);
|
||||
}
|
||||
|
||||
// 输出: 方法返回类型 => 200 + json
|
||||
var returnType = Unwrap(action.MethodInfo.ReturnType);
|
||||
if (returnType != typeof(void))
|
||||
{
|
||||
var responseType = new ApiResponseType
|
||||
{
|
||||
StatusCode = 200,
|
||||
Type = returnType,
|
||||
IsDefaultResponse = true
|
||||
};
|
||||
if (modelMetadata is not null)
|
||||
{
|
||||
responseType.ModelMetadata = modelMetadata.GetMetadataForType(returnType);
|
||||
}
|
||||
|
||||
responseType.ApiResponseFormats.Add(new ApiResponseFormat { MediaType = "application/json" });
|
||||
apiDescription.SupportedResponseTypes.Add(responseType);
|
||||
}
|
||||
|
||||
return apiDescription;
|
||||
}
|
||||
|
||||
private static Type Unwrap(Type type) =>
|
||||
type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)
|
||||
? type.GetGenericArguments()[0]
|
||||
: type;
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using SimApi.Attributes;
|
||||
using SimApi.Communications;
|
||||
using SimApi.Configurations;
|
||||
using SimApi.Helpers;
|
||||
using static SimApi.Helpers.SimApiError;
|
||||
|
||||
namespace SimApi.Controllers;
|
||||
|
||||
public class SimApiCommonController : SimApiBaseController
|
||||
public class SimApiCommonController(SimApiOptions simApiOptions) : SimApiBaseController
|
||||
{
|
||||
/// <summary>
|
||||
/// 错误回馈页面
|
||||
@@ -15,27 +18,38 @@ public class SimApiCommonController : SimApiBaseController
|
||||
/// <param name="code">错误代码</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("exception/{code:int}")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[SimApiDoc(ignore: true)]
|
||||
public void ExceptionHandler(int code)
|
||||
{
|
||||
Error(code);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 给前端的自定义信息
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost, HttpGet]
|
||||
public Dictionary<string, string> Versions()
|
||||
[HttpPost, SimApiDoc("公共", "获取自定义配置",groupNames: "*")]
|
||||
public Dictionary<string, object> WebConfig()
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
var resp = simApiOptions.WebConfig!.ToDictionary();
|
||||
if (simApiOptions.WebConfigIncludeVersion)
|
||||
{
|
||||
{ "SimApi", SimApiUtil.SimApiVersion },
|
||||
{ "App", SimApiUtil.AppVersion }
|
||||
};
|
||||
resp.Add("Versions", new Dictionary<string, string>
|
||||
{
|
||||
{ "SimApi", SimApiUtil.SimApiVersion },
|
||||
{ "App", SimApiUtil.AppVersion }
|
||||
});
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取已登录用户信息
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("/user/info"), SimApiAuth, SimApiDoc("认证", "获取已登录用户信息")]
|
||||
[HttpPost, SimApiAuth, SimApiDoc("认证", "获取已登录用户信息",groupNames: "*")]
|
||||
public SimApiLoginItem UserInfo() => LoginInfo;
|
||||
}
|
||||
+108
-43
@@ -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
|
||||
{
|
||||
SlidingExpiration = expireTime
|
||||
});
|
||||
_redisDb.KeyExpire(setCacheKey, expireTime.Value);
|
||||
|
||||
_cache.SetString(cacheKey, SimApiUtil.Json(loginItem),
|
||||
new DistributedCacheEntryOptions { SlidingExpiration = expireTime });
|
||||
|
||||
if (_redisDb != null)
|
||||
{
|
||||
_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));
|
||||
var ttl = _redisDb.KeyTimeToLive(cacheKey);
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", loginItem.Id);
|
||||
_redisDb.KeyExpire(setCacheKey, ttl);
|
||||
_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);
|
||||
@@ -83,26 +109,42 @@ public class SimApiAuth(IDistributedCache cache, IConnectionMultiplexer redis)
|
||||
/// <returns></returns>
|
||||
public SimApiLoginItem[] GetAllLogins(string userId)
|
||||
{
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", userId);
|
||||
var allLogins = _redisDb.SetMembers(setCacheKey).ToStringArray();
|
||||
var resp = new List<SimApiLoginItem>();
|
||||
foreach (var login in allLogins)
|
||||
if (_redisDb != null)
|
||||
{
|
||||
if (login != null)
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", userId);
|
||||
var allLogins = _redisDb.SetMembers(setCacheKey).ToStringArray();
|
||||
var resp = new List<SimApiLoginItem>();
|
||||
foreach (var login in allLogins)
|
||||
{
|
||||
var item = GetLogin(login);
|
||||
if (item != null)
|
||||
if (login != null)
|
||||
{
|
||||
resp.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
_redisDb.SetRemove(setCacheKey, login);
|
||||
var item = GetLogin(login);
|
||||
if (item != null)
|
||||
resp.Add(item);
|
||||
else
|
||||
_redisDb.SetRemove(setCacheKey, login);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp.ToArray();
|
||||
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>
|
||||
@@ -111,18 +153,32 @@ public class SimApiAuth(IDistributedCache cache, IConnectionMultiplexer redis)
|
||||
/// <param name="userId"></param>
|
||||
public void LogoutAll(string userId)
|
||||
{
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", userId);
|
||||
var allLogins = _redisDb.SetMembers(setCacheKey).ToStringArray();
|
||||
foreach (var login in allLogins)
|
||||
if (_redisDb != null)
|
||||
{
|
||||
if (login != null)
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", userId);
|
||||
var allLogins = _redisDb.SetMembers(setCacheKey).ToStringArray();
|
||||
foreach (var login in allLogins)
|
||||
{
|
||||
var cacheKey = TokenCacheKey.Replace("{token}", login);
|
||||
cache.Remove(cacheKey);
|
||||
if (login != null)
|
||||
{
|
||||
var cacheKey = TokenCacheKey.Replace("{token}", login);
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_redisDb.KeyDelete(setCacheKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -132,13 +188,22 @@ public class SimApiAuth(IDistributedCache cache, IConnectionMultiplexer redis)
|
||||
public void Logout(string token)
|
||||
{
|
||||
var item = GetLogin(token);
|
||||
if (item != null)
|
||||
|
||||
if (_redisDb != null)
|
||||
{
|
||||
var setCacheKey = TokenSetCacheKey.Replace("{userId}", item.Id);
|
||||
_redisDb.SetRemove(setCacheKey, token);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public static class SimApiError
|
||||
/// <param name="code">错误代码</param>
|
||||
/// <param name="message">错误描述</param>
|
||||
public static void ErrorWhenNull([NotNull] object? condition, int code = 404,
|
||||
string message = "请求的资源不存在")
|
||||
string message = "")
|
||||
{
|
||||
ErrorWhen(condition == null, code, message);
|
||||
}
|
||||
|
||||
+16
-14
@@ -12,15 +12,15 @@ namespace SimApi.Helpers;
|
||||
|
||||
public class SimApiHttpClient(SimApiOptions? apiOptions = null, ILogger<SimApiHttpClient>? logger = null)
|
||||
{
|
||||
public string Server { get; init; } = apiOptions?.SimApiHttpClientOptions.Server ?? string.Empty;
|
||||
public string AppId { get; init; } = apiOptions?.SimApiHttpClientOptions.AppId ?? string.Empty;
|
||||
public string AppKey { get; init; } = apiOptions?.SimApiHttpClientOptions.AppKey ?? string.Empty;
|
||||
public virtual string Server { get; init; } = apiOptions?.SimApiHttpClientOptions.Server ?? string.Empty;
|
||||
public virtual string AppId { get; init; } = apiOptions?.SimApiHttpClientOptions.AppId ?? string.Empty;
|
||||
public virtual string AppKey { get; init; } = apiOptions?.SimApiHttpClientOptions.AppKey ?? string.Empty;
|
||||
|
||||
public string SignName { get; init; } = "sign";
|
||||
public string TimestampName { get; init; } = "timestamp";
|
||||
public string NonceName { get; init; } = "nonce";
|
||||
public string? AppIdName { get; init; } = "appId";
|
||||
public string[] SignFields { get; init; } = [];
|
||||
public virtual string SignName { get; init; } = "sign";
|
||||
public virtual string TimestampName { get; init; } = "timestamp";
|
||||
public virtual string NonceName { get; init; } = "nonce";
|
||||
public virtual string? AppIdName { get; init; } = "appId";
|
||||
public virtual string[] SignFields { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 发起签名请求
|
||||
@@ -30,7 +30,7 @@ public class SimApiHttpClient(SimApiOptions? apiOptions = null, ILogger<SimApiHt
|
||||
/// <param name="queries"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T? SignQuery<T>(string url, object? body = null, Dictionary<string, string>? queries = null)
|
||||
public T SignQuery<T>(string url, object? body = null, Dictionary<string, string>? queries = null)
|
||||
{
|
||||
url = Server + url;
|
||||
var queryUrl = SignFields.Aggregate(string.Empty,
|
||||
@@ -60,14 +60,14 @@ public class SimApiHttpClient(SimApiOptions? apiOptions = null, ILogger<SimApiHt
|
||||
/// <param name="body"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T? AesQuery<T>(string url, object body)
|
||||
public T AesQuery<T>(string url, object body)
|
||||
{
|
||||
url = Server + url;
|
||||
if (!string.IsNullOrEmpty(AppIdName))
|
||||
{
|
||||
url += $"?{AppIdName}={AppId}";
|
||||
}
|
||||
|
||||
logger?.LogDebug($"[HTTPCLIENT请求][加密前BODY] {url}\n{SimApiUtil.Json(body)}\n");
|
||||
var req = new SimApiOneFieldRequest<string>
|
||||
{
|
||||
Data = SimApiAesUtil.Encrypt(SimApiUtil.Json(body), AppKey)
|
||||
@@ -83,8 +83,9 @@ public class SimApiHttpClient(SimApiOptions? apiOptions = null, ILogger<SimApiHt
|
||||
/// <param name="queries"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T? AesSignQuery<T>(string url, object body, Dictionary<string, string>? queries = null)
|
||||
public T AesSignQuery<T>(string url, object body, Dictionary<string, string>? queries = null)
|
||||
{
|
||||
logger?.LogDebug($"[HTTPCLIENT请求][加密前BODY] {url}\n{SimApiUtil.Json(body)}\n");
|
||||
var req = new SimApiOneFieldRequest<string>
|
||||
{
|
||||
Data = SimApiAesUtil.Encrypt(SimApiUtil.Json(body), AppKey)
|
||||
@@ -100,15 +101,16 @@ public class SimApiHttpClient(SimApiOptions? apiOptions = null, ILogger<SimApiHt
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="SimApiException"></exception>
|
||||
private T? Query<T>(string url, object? req)
|
||||
private T Query<T>(string url, object? req)
|
||||
{
|
||||
var http = new HttpClient();
|
||||
logger?.LogDebug($"[HTTPCLIENT请求] {url}\n{SimApiUtil.Json(req)}\n");
|
||||
var resp = http.PostAsJsonAsync(url, req).Result;
|
||||
logger?.LogDebug($"[HTTPCLIENT响应] {resp.Content.ReadAsStringAsync().Result}\n");
|
||||
SimApiError.ErrorWhenFalse(resp.IsSuccessStatusCode, (int)resp.StatusCode, $"HTTP ERROR: {resp.StatusCode}");
|
||||
var res = resp.Content.ReadFromJsonAsync<SimApiBaseResponse<T>>().Result;
|
||||
SimApiError.ErrorWhenNull(res, 500, "请求发生错误");
|
||||
SimApiError.ErrorWhen(res.Code != 200, res.Code, res.Message);
|
||||
return res.Data;
|
||||
return res.Data!;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public static class SimApiUtil
|
||||
{
|
||||
// ReferenceHandler = ReferenceHandler.Preserve,
|
||||
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
// DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
|
||||
@@ -45,11 +45,15 @@ public class SimApiExceptionMiddleware(
|
||||
response = string.IsNullOrEmpty(simEx.Message)
|
||||
? new SimApiBaseResponse(simEx.Code)
|
||||
: new SimApiBaseResponse(simEx.Code, simEx.Message);
|
||||
if (context.Response.StatusCode == 404)
|
||||
{
|
||||
response.Message = "接口不存在";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log.LogError(ex, "服务器异常");
|
||||
response = new SimApiBaseResponse(500, "服务器错误");
|
||||
log.LogError(ex, ex.Message);
|
||||
response = new SimApiBaseResponse(500);
|
||||
}
|
||||
|
||||
await ErrorResponseAsync(context, response);
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
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.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SimApi.Configurations;
|
||||
using SimApi.Helpers;
|
||||
|
||||
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 ) =>");
|
||||
if (options.RequestStringLogLength == 0)
|
||||
{
|
||||
logMessage.AppendLine(requestBodyText);
|
||||
}
|
||||
else
|
||||
{
|
||||
// GET/DELETE 等无 body 的请求, 或表单/文件上传等非 JSON body:
|
||||
// 不做长度裁剪, 原样记录, 避免空 JSON 解析抛异常
|
||||
logMessage.AppendLine(TrimJsonStringFields(requestBodyText, options.RequestStringLogLength));
|
||||
}
|
||||
|
||||
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.ResponseStringLogLength > 0)
|
||||
{
|
||||
// 与请求体相同的字段级裁剪: JSON 按字段截断超长字符串, 非 JSON/空响应原样记录
|
||||
logMessage.Append(TrimJsonStringFields(responseText, options.ResponseStringLogLength));
|
||||
}
|
||||
else 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();
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -12,13 +12,13 @@
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.22" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.23" />
|
||||
<PackageReference Include="Hangfire.Console" Version="1.4.3"/>
|
||||
<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.12.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.9" />
|
||||
<PackageReference Include="Minio" Version="7.0.0" />
|
||||
<PackageReference Include="MQTTnet" Version="5.0.1.1416"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.0" />
|
||||
<PackageReference Include="MQTTnet" Version="5.1.0.1559" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+98
-23
@@ -9,14 +9,18 @@ using Hangfire.Redis.StackExchange;
|
||||
using SimApi.Helpers;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.OpenApi;
|
||||
using SimApi.Middlewares;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SimApi.Attributes;
|
||||
using SimApi.AuthSDK;
|
||||
using SimApi.Configurations;
|
||||
using SimApi.Controllers;
|
||||
using SimApi.Interfaces;
|
||||
using SimApi.Logger;
|
||||
using SimApi.SwaggerFilters;
|
||||
@@ -35,11 +39,22 @@ public static class SimApiExtensions
|
||||
{
|
||||
var simApiOptions = new SimApiOptions();
|
||||
options?.Invoke(simApiOptions);
|
||||
simApiOptions.WebConfig ??= new();
|
||||
builder.AddSingleton(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>();
|
||||
}
|
||||
|
||||
@@ -208,6 +223,7 @@ public static class SimApiExtensions
|
||||
x.OperationFilter<SimApiSignOperationFilter>();
|
||||
x.OperationFilter<AesBodyOperationFilter>();
|
||||
x.SchemaFilter<GlobalDynamicObjectSchemaFilter>();
|
||||
x.SchemaFilter<DictionarySchemaFilter>();
|
||||
x.DocumentFilter<RemoveEmptyTagsFilter>();
|
||||
if (simApiOptions.EnableSimApiAuth)
|
||||
{
|
||||
@@ -216,15 +232,42 @@ public static class SimApiExtensions
|
||||
|
||||
x.DocInclusionPredicate((docName, apiDesc) =>
|
||||
{
|
||||
// 获取接口标记的 GroupName(未标记则为 null)
|
||||
var actionGroupName = apiDesc.ActionDescriptor.EndpointMetadata
|
||||
var metadata = apiDesc.ActionDescriptor.EndpointMetadata;
|
||||
|
||||
// 类/方法上的 [SimApiDoc] 标注统一控制文档归属
|
||||
var docAttrs = metadata.OfType<SimApiDocAttribute>().ToArray();
|
||||
|
||||
// Ignore: 不出现在任何文档(路由不受影响)
|
||||
if (docAttrs.Any(a => a.Ignore))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// GroupNames: 逗号分隔的文档组列表; "*" 表示所有文档;
|
||||
// 标注在类上则整类生效, 方法级标注可覆盖
|
||||
var groupNames = docAttrs
|
||||
.Select(a => a.GroupNames)
|
||||
.FirstOrDefault(g => !string.IsNullOrWhiteSpace(g));
|
||||
if (groupNames != null)
|
||||
{
|
||||
var groups = groupNames.Split(',',
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return groups.Contains(SimApiDocAttribute.AllGroups) || groups.Contains(docName);
|
||||
}
|
||||
|
||||
// 兼容: ApiExplorerSettings.GroupName 单组标注
|
||||
var actionGroupName = metadata
|
||||
.OfType<ApiExplorerSettingsAttribute>()
|
||||
.FirstOrDefault()?.GroupName;
|
||||
|
||||
// 情况1:接口未标记任何 GroupName(actionGroupName 为 null)
|
||||
// 未标注任何分组 → 放入配置为 IsDefault 的文档组;
|
||||
// 未配置 IsDefault 时依次回退 id 为 "api" 的组 / 第一个组
|
||||
if (actionGroupName == null)
|
||||
{
|
||||
return docName == "api"; // 未分组接口只属于默认分组v1
|
||||
var defaultGroup = docOptions.ApiGroups.FirstOrDefault(g => g.IsDefault)
|
||||
?? docOptions.ApiGroups.FirstOrDefault(g => g.Id == "api")
|
||||
?? docOptions.ApiGroups.FirstOrDefault();
|
||||
return defaultGroup != null && docName == defaultGroup.Id;
|
||||
}
|
||||
|
||||
return docName == actionGroupName;
|
||||
@@ -295,6 +338,13 @@ public static class SimApiExtensions
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 让 SimApiRouteOptions 配置的内置约定路由(UserInfo/Logout/WebConfig)
|
||||
// 也能出现在文档中: 通过 IApiDescriptionProvider 手工产出 ApiDescription,
|
||||
// 请求/响应 schema 由 Swashbuckle 自动分析, [SimApiDoc] 注解自动生效。
|
||||
builder.TryAddEnumerable(
|
||||
ServiceDescriptor.Transient<IApiDescriptionProvider,
|
||||
SimApiBuiltInRoutesDescriptionProvider>());
|
||||
}
|
||||
|
||||
// 使用Header转发,应对代理后获取真实ip
|
||||
@@ -304,7 +354,6 @@ public static class SimApiExtensions
|
||||
{
|
||||
fwOptions.ForwardedHeaders = ForwardedHeaders.All;
|
||||
fwOptions.KnownNetworks.Clear();
|
||||
fwOptions.KnownProxies.Clear();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -328,7 +377,17 @@ public static class SimApiExtensions
|
||||
});
|
||||
}
|
||||
|
||||
builder.AddSingleton(simApiOptions);
|
||||
if (simApiOptions.EnableSimApiAuthGate)
|
||||
{
|
||||
builder.AddSingleton<SimApiAuthClient>();
|
||||
builder.AddSingleton<SimApiAuthCenter>();
|
||||
if (simApiOptions.SimApiAuthCenterOptions.UseIam)
|
||||
{
|
||||
builder.AddSingleton<SimApiAuthIam>();
|
||||
}
|
||||
}
|
||||
|
||||
builder.AddSingleton(simApiOptions.SimApiRequestLogOptions);
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -347,6 +406,11 @@ public static class SimApiExtensions
|
||||
logger.LogInformation("开始配置 RedisCache ...");
|
||||
}
|
||||
|
||||
if (options.EnableSimApiCache)
|
||||
{
|
||||
logger.LogInformation("开始配置SimApiCache...");
|
||||
}
|
||||
|
||||
//请求一下检测存储错误
|
||||
if (options.EnableSimApiStorage)
|
||||
{
|
||||
@@ -411,17 +475,20 @@ public static class SimApiExtensions
|
||||
logger.LogInformation(msg);
|
||||
}
|
||||
|
||||
if (options.EnableSimApiGateAuth)
|
||||
if (options.EnableSimApiAuthGate)
|
||||
{
|
||||
logger.LogInformation("开始配置SimApiGateAuth...");
|
||||
if (string.IsNullOrEmpty(options.SimApiGateAuthOptions.AppId) ||
|
||||
string.IsNullOrEmpty(options.SimApiGateAuthOptions.AppKey))
|
||||
logger.LogInformation("开始配置SimApiAuthGate...");
|
||||
if (string.IsNullOrEmpty(options.SimApiAuthCenterOptions.AppId) ||
|
||||
string.IsNullOrEmpty(options.SimApiAuthCenterOptions.AppKey))
|
||||
{
|
||||
logger.LogCritical("必须配置Gate的AppId和AppKey才能启用SimApiGateAuth");
|
||||
logger.LogCritical("必须配置AuthGate的AppId和AppKey才能启用SimApiAuthGate");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.UseMiddleware<SimApiGateAuthMiddleware>();
|
||||
if (options.SimApiAuthCenterOptions.UseMiddleware)
|
||||
{
|
||||
builder.UseMiddleware<SimApiAuthCenterMiddleware>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,21 +496,21 @@ public static class SimApiExtensions
|
||||
{
|
||||
logger.LogInformation("开始配置SimApiAuth...");
|
||||
builder.UseMiddleware<SimApiAuthMiddleware>();
|
||||
builder.MapControllerRoute(name: "Logout", pattern: "/auth/logout",
|
||||
defaults: new
|
||||
{
|
||||
controller = "SimApiAuth",
|
||||
action = "Logout"
|
||||
});
|
||||
}
|
||||
|
||||
if (options.EnableVersionUrl)
|
||||
// 注册内置Route(UserInfo/Logout/WebConfig)。
|
||||
// 路径统一由 SimApiBuiltInRoutes 路由表提供: 配置为 null/空 的端点不注册。
|
||||
foreach (var route in SimApiBuiltInRoutes.Get(options.SimApiRouteOptions))
|
||||
{
|
||||
builder.MapControllerRoute(name: "Versions", pattern: "/versions",
|
||||
logger.LogInformation("注册内置Route: {RouteName} => {RoutePath} [{HttpMethods}]", route.RouteName,
|
||||
route.Path, string.Join(", ", route.HttpMethods));
|
||||
builder.MapControllerRoute(
|
||||
name: route.RouteName,
|
||||
pattern: route.Path,
|
||||
defaults: new
|
||||
{
|
||||
controller = "SimApiCommon",
|
||||
action = "Versions"
|
||||
controller = route.Controller,
|
||||
action = route.Action
|
||||
});
|
||||
}
|
||||
|
||||
@@ -451,12 +518,14 @@ public static class SimApiExtensions
|
||||
{
|
||||
logger.LogInformation("开始配置SimApiDoc...");
|
||||
var docOptions = options.SimApiDocOptions;
|
||||
builder.UseSwagger(x => x.RouteTemplate = "/swagger/{documentName}.json")
|
||||
builder.UseSwagger(x => x.RouteTemplate = $"/{docOptions.UrlPrefix}/{{documentName}}.json")
|
||||
.UseSwaggerUI(x =>
|
||||
{
|
||||
x.RoutePrefix = docOptions.UrlPrefix;
|
||||
x.DocumentTitle = docOptions.DocumentTitle;
|
||||
foreach (var group in docOptions.ApiGroups)
|
||||
{
|
||||
// 相对端点: 页面位于 /{UrlPrefix}/, 自动解析为 /{UrlPrefix}/{group.Id}.json
|
||||
x.SwaggerEndpoint($"{group.Id}.json", name: group.Name);
|
||||
}
|
||||
|
||||
@@ -465,6 +534,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