Compare commits

..
4 Commits
Author SHA1 Message Date
xrain c7373da691 add aesbody for get aes cryptbody 2025-11-02 18:58:01 +08:00
xrain 9a8d14142a version move to common 2025-11-01 21:38:55 +08:00
xrain cb89a1a003 doc only appear in current group 2025-11-01 18:40:51 +08:00
xrain 462bce50cc fix pack bug 2025-11-01 15:50:11 +08:00
10 changed files with 381 additions and 61 deletions
+18
View File
@@ -0,0 +1,18 @@
using System;
using Microsoft.AspNetCore.Mvc;
using SimApi.ModelBinders;
namespace SimApi.Attributes;
[AttributeUsage(AttributeTargets.Parameter)]
public class AesBodyAttribute : ModelBinderAttribute
{
public Type KeyProvider { get; set; } = typeof(AesBodyProviderBase);
public AesBodyAttribute()
{
// 指定使用自定义的模型绑定器
BinderType = typeof(AesBodyModelBinder);
Name = KeyProvider.FullName;
}
}
+113
View File
@@ -0,0 +1,113 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using SimApi.Exceptions;
using SimApi.Helpers;
using SimApi.ModelBinders;
namespace SimApi.Attributes;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SimApiSignAttribute : ActionFilterAttribute
{
protected Type KeyProvider { get; set; } = typeof(SimApiSignProviderBase);
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.RequestServices.GetService(KeyProvider) is not SimApiSignProviderBase keyProvider)
{
throw new SimApiException(400, "未配置签名器");
}
string? appId = null;
if (!string.IsNullOrEmpty(keyProvider.AppIdName))
{
appId = context.HttpContext.Request.Query[keyProvider.AppIdName]
.FirstOrDefault() ?? context.HttpContext.Request.Headers[keyProvider.AppIdName]
.FirstOrDefault();
if (string.IsNullOrEmpty(appId))
{
throw new SimApiException(400, $"获取{keyProvider.AppIdName}失败");
}
}
// 4. 通过接口获取密钥(解耦的核心:不再直接依赖数据库)
var key = keyProvider.GetKey(appId);
if (string.IsNullOrEmpty(key))
{
throw new SimApiException(400, "获取签名KEY失败");
}
var timestamp = context.HttpContext.Request.Query[keyProvider.TimestampName]
.FirstOrDefault() ?? context.HttpContext.Request.Headers[keyProvider.TimestampName]
.FirstOrDefault();
if (string.IsNullOrEmpty(timestamp))
{
throw new SimApiException(400, $"{keyProvider.TimestampName}不能为空");
}
var nonce = context.HttpContext.Request.Query[keyProvider.NonceName]
.FirstOrDefault() ?? context.HttpContext.Request.Headers[keyProvider.NonceName]
.FirstOrDefault();
if (string.IsNullOrEmpty(nonce))
{
throw new SimApiException(400, $"{keyProvider.NonceName}不能为空");
}
if (!int.TryParse(timestamp, out var ts))
{
throw new SimApiException(400, $"{keyProvider.TimestampName}格式错误");
}
if (keyProvider.QueryExpires != 0)
{
if (ts > SimApiUtil.TimestampNow + 2)
{
throw new SimApiException(400, "请校准本地时间");
}
if (ts + keyProvider.QueryExpires < SimApiUtil.TimestampNow)
{
throw new SimApiException(400, "请求已过期");
}
if (keyProvider.DuplicateRequestProtection)
{
var cache = context.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();
if (cache.GetString("SignQuery:" + nonce) == null)
{
cache.SetString("SignQuery:" + nonce, timestamp, new DistributedCacheEntryOptions()
{
SlidingExpiration = TimeSpan.FromSeconds(keyProvider.QueryExpires + 2)
});
}
else
{
throw new SimApiException(400, "重复请求");
}
}
}
var signStr = string.Empty;
foreach (var item in keyProvider.SignFields)
{
signStr += $"{item}=";
signStr += context.HttpContext.Request.Query[item]
.FirstOrDefault() ?? context.HttpContext.Request.Headers[item]
.FirstOrDefault();
signStr += "&";
}
signStr += $"{keyProvider.TimestampName}={ts}&{keyProvider.NonceName}={nonce}&{key}";
var sign = context.HttpContext.Request.Query[keyProvider.SignName]
.FirstOrDefault() ?? context.HttpContext.Request.Headers[keyProvider.SignName]
.FirstOrDefault();
if (SimApiUtil.Md5(signStr) != sign)
{
throw new SimApiException(400, "签名错误");
}
}
}
+11 -16
View File
@@ -6,22 +6,22 @@ namespace SimApi.Configurations;
/// <summary>
/// 文档组配置
/// </summary>
public class SimApiDocGroupOption
public class SimApiDocGroupOption(string id, string name, string description = "")
{
/// <summary>
/// 文档标识
/// </summary>
public string Id { get; set; } = null!;
public string Id { get; set; } = id;
/// <summary>
/// 文档名称
/// </summary>
public string Name { get; set; } = null!;
public string Name { get; set; } = name;
/// <summary>
/// 文档描述
/// </summary>
public string Description { get; set; } = null!;
public string Description { get; set; } = description!;
}
/// <summary>
@@ -29,7 +29,7 @@ public class SimApiDocGroupOption
/// </summary>
public class SimApiAuthOption
{
public string[] Type { get; set; } = new[] { "SimApiAuth" };
public string[] Type { get; set; } = ["SimApiAuth"];
public string Description { get; set; } = "认证服务器颁发的AccessToken";
@@ -48,20 +48,15 @@ public class SimApiDocOptions
/// <summary>
/// 文档组配置
/// </summary>
public SimApiDocGroupOption[] ApiGroups { get; set; } = new[]
{
new SimApiDocGroupOption
{
Id = "api",
Name = "Api",
Description = "Api接口文档"
}
};
public SimApiDocGroupOption[] ApiGroups { get; set; } =
[
new("api", "Api", "Api接口文档")
];
/// <summary>
/// 授权配置
/// </summary>
public SimApiAuthOption ApiAuth { get; set; } = new SimApiAuthOption();
public SimApiAuthOption ApiAuth { get; set; } = new();
/// <summary>
/// 文档页面标题
@@ -71,5 +66,5 @@ public class SimApiDocOptions
/// <summary>
/// 接口支持的调用方式
/// </summary>
public SubmitMethod[] SupportedMethod { get; set; } = new[] { SubmitMethod.Post };
public SubmitMethod[] SupportedMethod { get; set; } = [SubmitMethod.Post];
}
+49
View File
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using SimApi.Attributes;
using SimApi.Communications;
using SimApi.Helpers;
namespace SimApi.Controllers;
public class SimApiAuthController(SimApiAuth auth) : SimApiBaseController
{
/// <summary>
/// 检测用户登陆的控制器
/// </summary>
/// <returns></returns>
[HttpPost, SimApiDoc("认证", "检测登陆")]
public SimApiBaseResponse<string> CheckLogin()
{
ErrorWhenNull(LoginInfo, 401, "未登录");
return new SimApiBaseResponse<string>
{
Data = LoginInfo.Id
};
}
/// <summary>
/// 退出登陆
/// </summary>
/// <returns></returns>
[HttpPost, SimApiDoc("认证", "退出登陆")]
public SimApiBaseResponse Logout()
{
string? token = null;
if (Request.Headers.TryGetValue("Token", out var value))
{
token = value;
}
auth.Logout(token!);
return new SimApiBaseResponse();
}
[HttpPost, SimApiAuth]
public SimApiBaseResponse<SimApiLoginItem> UserInfo()
{
return new SimApiBaseResponse<SimApiLoginItem>(LoginInfo);
}
}
+2 -40
View File
@@ -1,12 +1,11 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using SimApi.Attributes;
using SimApi.Communications;
using SimApi.Helpers;
namespace SimApi.Controllers;
public class SimApiCommonController(SimApiAuth auth) : SimApiBaseController
public class SimApiCommonController : SimApiBaseController
{
/// <summary>
/// 错误回馈页面
@@ -20,37 +19,6 @@ public class SimApiCommonController(SimApiAuth auth) : SimApiBaseController
return new SimApiBaseResponse(code);
}
/// <summary>
/// 检测用户登陆的控制器
/// </summary>
/// <returns></returns>
[HttpPost, SimApiDoc("认证", "检测登陆")]
public SimApiBaseResponse<string> CheckLogin()
{
ErrorWhenNull(LoginInfo, 401, "未登录");
return new SimApiBaseResponse<string>
{
Data = LoginInfo.Id
};
}
/// <summary>
/// 退出登陆
/// </summary>
/// <returns></returns>
[HttpPost, SimApiDoc("认证", "退出登陆")]
public SimApiBaseResponse Logout()
{
string? token = null;
if (Request.Headers.TryGetValue("Token", out var value))
{
token = value;
}
auth.Logout(token!);
return new SimApiBaseResponse();
}
[HttpPost, HttpGet]
public SimApiBaseResponse<Dictionary<string, string>> Versions()
@@ -64,10 +32,4 @@ public class SimApiCommonController(SimApiAuth auth) : SimApiBaseController
}
};
}
[HttpPost, SimApiAuth]
public SimApiBaseResponse<SimApiLoginItem> UserInfo()
{
return new SimApiBaseResponse<SimApiLoginItem>(LoginInfo);
}
}
+106
View File
@@ -0,0 +1,106 @@
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using SimApi.Communications;
using SimApi.Helpers;
namespace SimApi.ModelBinders;
public class AesBodyModelBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var request = bindingContext.HttpContext.Request;
request.EnableBuffering();
using var reader = new StreamReader(request.Body, leaveOpen: true);
var requestBody = await reader.ReadToEndAsync();
request.Body.Position = 0;
if (string.IsNullOrEmpty(requestBody))
{
bindingContext.ModelState.AddModelError("", "请求体不能为空");
bindingContext.Result = ModelBindingResult.Failed();
return;
}
SimApiOneFieldRequest<string>? aesRequest;
try
{
aesRequest = JsonSerializer.Deserialize<SimApiOneFieldRequest<string>>(requestBody, SimApiUtil.JsonOption);
}
catch (JsonException ex)
{
bindingContext.ModelState.AddModelError("", $"请求体格式错误:{ex.Message}");
bindingContext.Result = ModelBindingResult.Failed();
return;
}
if (aesRequest == null || string.IsNullOrEmpty(aesRequest.Data))
{
bindingContext.ModelState.AddModelError("", "请求体缺少密文Data字段");
bindingContext.Result = ModelBindingResult.Failed();
return;
}
// 3. 根据配置获取appId(参考上一节代码)
var keyProvider =
bindingContext.HttpContext.RequestServices.GetService(Type.GetType(bindingContext.BinderModelName!)!) as
AesBodyProviderBase;
string? appId = null;
if (!string.IsNullOrEmpty(keyProvider!.AppIdName))
{
appId = bindingContext.HttpContext.Request.Query[keyProvider.AppIdName]
.FirstOrDefault() ?? bindingContext.HttpContext.Request.Headers[keyProvider.AppIdName]
.FirstOrDefault();
if (string.IsNullOrEmpty(appId))
{
bindingContext.ModelState.AddModelError("",
$"未找到{keyProvider.AppIdName}");
bindingContext.Result = ModelBindingResult.Failed();
return;
}
}
// 4. 通过接口获取密钥(解耦的核心:不再直接依赖数据库)
var key = keyProvider.GetKey(appId);
if (string.IsNullOrEmpty(key))
{
bindingContext.ModelState.AddModelError("", "获取密钥失败(应用不存在或密钥未配置)");
bindingContext.Result = ModelBindingResult.Failed();
return;
}
// 5. 解密和反序列化(保持原有逻辑)
try
{
var jsonStr = SimApiAesUtil.Decrypt(aesRequest.Data, key);
if (string.IsNullOrEmpty(jsonStr))
{
bindingContext.ModelState.AddModelError("", "解密失败");
bindingContext.Result = ModelBindingResult.Failed();
return;
}
var targetType = bindingContext.ModelType;
var deserializedModel = JsonSerializer.Deserialize(jsonStr, targetType, SimApiUtil.JsonOption);
if (deserializedModel == null)
{
bindingContext.ModelState.AddModelError("", "反序列化失败");
bindingContext.Result = ModelBindingResult.Failed();
return;
}
bindingContext.Result = ModelBindingResult.Success(deserializedModel);
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError("", $"处理异常:{ex.Message}");
bindingContext.Result = ModelBindingResult.Failed();
}
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace SimApi.ModelBinders;
/// <summary>
/// 密钥提供器接口(抽象密钥获取逻辑)
/// </summary>
public abstract class AesBodyProviderBase
{
public string? AppIdName { get; set; } = "appId";
/// <summary>
/// 根据appId获取对应的密钥
/// </summary>
/// <param name="appId">应用ID</param>
/// <returns>密钥(返回null表示获取失败)</returns>
public abstract string? GetKey(string? appId);
}
+43
View File
@@ -0,0 +1,43 @@
namespace SimApi.ModelBinders;
public abstract class SimApiSignProviderBase
{
/// <summary>
/// appId字段的名称
/// </summary>
public string? AppIdName { get; set; } = "appId";
/// <summary>
/// 时间戳的字段名
/// </summary>
public string TimestampName { get; set; } = "timestamp";
/// <summary>
/// 随机字符串的字段名
/// </summary>
public string NonceName { get; set; } = "nonce";
/// <summary>
/// 签名的字段名
/// </summary>
public string SignName { get; set; } = "sign";
/// <summary>
/// 请求过期时间, 如果为0, 不校验timestamp
/// </summary>
public int QueryExpires { get; set; } = 5;
/// <summary>
/// 如果开启,必须配置redis, 每次请求将会缓存nonce
/// </summary>
public bool DuplicateRequestProtection { get; set; } = true;
public string[] SignFields { get; set; } = ["appId"];
/// <summary>
/// 根据appId获取对应的密钥
/// </summary>
/// <param name="appId">应用ID</param>
/// <returns>密钥(返回null表示获取失败)</returns>
public abstract string? GetKey(string? appId);
}
+2 -2
View File
@@ -3,20 +3,20 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<PackOnBuild>true</PackOnBuild>
<IsPackable>true</IsPackable>
<Version>0.0.0</Version>
<Authors>xRain@SimcuTeam</Authors>
<Description>AspNetCore一个方便的API文档,捕获异常,统一输入输出的API类库</Description>
<PackageId>Simcu.SimApi</PackageId>
<Nullable>enable</Nullable>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<Folder Include="Communications\"/>
<Folder Include="Exceptions\"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.21" />
<PackageReference Include="Hangfire.Console" Version="1.4.3"/>
<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.12.0"/>
+20 -3
View File
@@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using SimApi.Middlewares;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SimApi.Attributes;
@@ -128,6 +129,22 @@ public static class SimApiExtensions
x.OperationFilter<SimApiAuthOperationFilter>();
}
x.DocInclusionPredicate((docName, apiDesc) =>
{
// 获取接口标记的 GroupName(未标记则为 null
var actionGroupName = apiDesc.ActionDescriptor.EndpointMetadata
.OfType<ApiExplorerSettingsAttribute>()
.FirstOrDefault()?.GroupName;
// 情况1:接口未标记任何 GroupNameactionGroupName 为 null
if (actionGroupName == null)
{
return docName == "api"; // 未分组接口只属于默认分组v1
}
return docName == actionGroupName;
});
x.EnableAnnotations();
var haveOauth = false;
var oauthFlows = new OpenApiOAuthFlows();
@@ -308,19 +325,19 @@ public static class SimApiExtensions
builder.MapControllerRoute(name: "GetUserInfo", pattern: "/user/info",
defaults: new
{
controller = "SimApiCommon",
controller = "SimApiAuth",
action = "UserInfo"
});
builder.MapControllerRoute(name: "CheckLogin", pattern: "/auth/check",
defaults: new
{
controller = "SimApiCommon",
controller = "SimApiAuth",
action = "CheckLogin"
});
builder.MapControllerRoute(name: "Logout", pattern: "/auth/logout",
defaults: new
{
controller = "SimApiCommon",
controller = "SimApiAuth",
action = "Logout"
});
if (options.EnableCoceSdk)