From 65451d7b80a6472796c6927667f642fc181b9400 Mon Sep 17 00:00:00 2001 From: xRain Date: Sun, 2 Nov 2025 22:14:34 +0800 Subject: [PATCH] fix swagger, add simapihttpclient --- Attributes/AesBodyAttribute.cs | 2 + Attributes/SimApiSignAttribute.cs | 7 +- Helpers/SimApiHttpClient.cs | 67 +++++++++++++ ModelBinders/AesBodyProviderBase.cs | 4 +- ModelBinders/SimApiSignProviderBase.cs | 16 +-- SimApiExtensions.cs | 5 +- SwaggerFilters/AesBodyOperationFilter.cs | 52 ++++++++++ .../SimApiAuthOperationFilter.cs | 3 +- .../SimApiResponseOperationFilter.cs | 4 +- SwaggerFilters/SimApiSignOperationFilter.cs | 97 +++++++++++++++++++ 10 files changed, 243 insertions(+), 14 deletions(-) create mode 100644 Helpers/SimApiHttpClient.cs create mode 100644 SwaggerFilters/AesBodyOperationFilter.cs rename {Helpers => SwaggerFilters}/SimApiAuthOperationFilter.cs (97%) rename Helpers/SimApiResponseSchemaFilter.cs => SwaggerFilters/SimApiResponseOperationFilter.cs (97%) create mode 100644 SwaggerFilters/SimApiSignOperationFilter.cs diff --git a/Attributes/AesBodyAttribute.cs b/Attributes/AesBodyAttribute.cs index 4773b20..fa32251 100644 --- a/Attributes/AesBodyAttribute.cs +++ b/Attributes/AesBodyAttribute.cs @@ -1,5 +1,6 @@ using System; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using SimApi.ModelBinders; namespace SimApi.Attributes; @@ -8,6 +9,7 @@ namespace SimApi.Attributes; public class AesBodyAttribute : ModelBinderAttribute { public Type KeyProvider { get; set; } = typeof(AesBodyProviderBase); + public override BindingSource BindingSource => BindingSource.Body; public AesBodyAttribute() { diff --git a/Attributes/SimApiSignAttribute.cs b/Attributes/SimApiSignAttribute.cs index 9c2b0b1..3979dd5 100644 --- a/Attributes/SimApiSignAttribute.cs +++ b/Attributes/SimApiSignAttribute.cs @@ -12,7 +12,7 @@ namespace SimApi.Attributes; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class SimApiSignAttribute : ActionFilterAttribute { - protected Type KeyProvider { get; set; } = typeof(SimApiSignProviderBase); + public Type KeyProvider { get; set; } = typeof(SimApiSignProviderBase); public override void OnActionExecuting(ActionExecutingContext context) { @@ -101,6 +101,11 @@ public class SimApiSignAttribute : ActionFilterAttribute signStr += "&"; } + if (!string.IsNullOrEmpty(keyProvider.AppIdName)) + { + signStr += $"{keyProvider.AppIdName}={appId}&"; + } + signStr += $"{keyProvider.TimestampName}={ts}&{keyProvider.NonceName}={nonce}&{key}"; var sign = context.HttpContext.Request.Query[keyProvider.SignName] .FirstOrDefault() ?? context.HttpContext.Request.Headers[keyProvider.SignName] diff --git a/Helpers/SimApiHttpClient.cs b/Helpers/SimApiHttpClient.cs new file mode 100644 index 0000000..265b599 --- /dev/null +++ b/Helpers/SimApiHttpClient.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using SimApi.Communications; + +namespace SimApi.Helpers; + +public class SimApiHttpClient(string? appId, string appKey) +{ + 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 T? SignQuery(string url, object body, Dictionary? queries = null) + { + var queryUrl = SignFields.Aggregate(string.Empty, + (current, signField) => current + $"{signField}={queries?[signField]}&"); + if (!string.IsNullOrEmpty(AppIdName)) + { + queryUrl += $"{AppIdName}={appId}&"; + } + + queryUrl += $"{TimestampName}={(int)SimApiUtil.TimestampNow}&{NonceName}={Guid.NewGuid()}"; + var signStr = $"{queryUrl}&{appKey}"; + var path = $"{url}?{queryUrl}&{SignName}={SimApiUtil.Md5(signStr)}"; + + if (queries != null) + { + path = queries.Where(q => !SignFields.Contains(q.Key)) + .Aggregate(path, (current, q) => current + $"&{q.Key}={q.Value}"); + } + + var http = new HttpClient(); + var resp = http.PostAsJsonAsync(path, body).Result; + return resp.Content.ReadFromJsonAsync().Result; + } + + public T? AesQuery(string url, object body) + { + if (!string.IsNullOrEmpty(AppIdName)) + { + url += $"?{AppIdName}={appId}"; + } + + var req = new SimApiOneFieldRequest + { + Data = SimApiAesUtil.Encrypt(SimApiUtil.Json(body), appKey) + }; + var http = new HttpClient(); + var resp = http.PostAsJsonAsync(url, req).Result; + return resp.Content.ReadFromJsonAsync().Result; + } + + public T? AesSignQuery(string url, object body, Dictionary? queries = null) + { + var req = new SimApiOneFieldRequest + { + Data = SimApiAesUtil.Encrypt(SimApiUtil.Json(body), appKey) + }; + return SignQuery(url, req, queries); + } +} \ No newline at end of file diff --git a/ModelBinders/AesBodyProviderBase.cs b/ModelBinders/AesBodyProviderBase.cs index 9fbe90b..d662209 100644 --- a/ModelBinders/AesBodyProviderBase.cs +++ b/ModelBinders/AesBodyProviderBase.cs @@ -1,3 +1,5 @@ +using SimApi.Exceptions; + namespace SimApi.ModelBinders; /// @@ -5,7 +7,7 @@ namespace SimApi.ModelBinders; /// public abstract class AesBodyProviderBase { - public string? AppIdName { get; set; } = "appId"; + public virtual string? AppIdName { get; set; } = "appId"; /// diff --git a/ModelBinders/SimApiSignProviderBase.cs b/ModelBinders/SimApiSignProviderBase.cs index 4af7d4c..2aebe5f 100644 --- a/ModelBinders/SimApiSignProviderBase.cs +++ b/ModelBinders/SimApiSignProviderBase.cs @@ -1,3 +1,5 @@ +using SimApi.Exceptions; + namespace SimApi.ModelBinders; public abstract class SimApiSignProviderBase @@ -5,34 +7,34 @@ public abstract class SimApiSignProviderBase /// /// appId字段的名称 /// - public string? AppIdName { get; set; } = "appId"; + public virtual string? AppIdName { get; set; } = "appId"; /// /// 时间戳的字段名 /// - public string TimestampName { get; set; } = "timestamp"; + public virtual string TimestampName { get; set; } = "timestamp"; /// /// 随机字符串的字段名 /// - public string NonceName { get; set; } = "nonce"; + public virtual string NonceName { get; set; } = "nonce"; /// /// 签名的字段名 /// - public string SignName { get; set; } = "sign"; + public virtual string SignName { get; set; } = "sign"; /// /// 请求过期时间, 如果为0, 不校验timestamp /// - public int QueryExpires { get; set; } = 5; + public virtual int QueryExpires { get; set; } = 5; /// /// 如果开启,必须配置redis, 每次请求将会缓存nonce /// - public bool DuplicateRequestProtection { get; set; } = true; + public virtual bool DuplicateRequestProtection { get; set; } = true; - public string[] SignFields { get; set; } = ["appId"]; + public virtual string[] SignFields { get; set; } = []; /// /// 根据appId获取对应的密钥 diff --git a/SimApiExtensions.cs b/SimApiExtensions.cs index de85b44..0508a04 100644 --- a/SimApiExtensions.cs +++ b/SimApiExtensions.cs @@ -19,6 +19,7 @@ using SimApi.Attributes; using SimApi.CoceSdk; using SimApi.Configurations; using SimApi.Logger; +using SimApi.SwaggerFilters; namespace SimApi; @@ -123,7 +124,9 @@ public static class SimApiExtensions } x.CustomSchemaIds(type => type.FullName?.Replace("+", ".")); - x.OperationFilter(); + x.OperationFilter(); + x.OperationFilter(); + x.OperationFilter(); if (simApiOptions.EnableSimApiAuth) { x.OperationFilter(); diff --git a/SwaggerFilters/AesBodyOperationFilter.cs b/SwaggerFilters/AesBodyOperationFilter.cs new file mode 100644 index 0000000..59b2164 --- /dev/null +++ b/SwaggerFilters/AesBodyOperationFilter.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using SimApi.Attributes; + +namespace SimApi.SwaggerFilters; + +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Reflection; + +/// +/// 自定义 Swagger 过滤器:将标注 [AesBody] 的参数显示在 Request Body 中 +/// +public class AesBodyOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + foreach (var parameter in context.ApiDescription.ParameterDescriptions) + { + var hasAesBodyAttr = parameter.ParameterInfo() + .GetCustomAttribute() != null; + if (!hasAesBodyAttr) continue; + // 1. 移除默认的 Query 参数描述(如果存在) + var queryParam = operation.Parameters + .FirstOrDefault(p => p.Name == parameter.Name); + if (queryParam != null) + { + operation.Parameters.Remove(queryParam); + } + + // 2. 添加 Body 参数描述 + // 获取参数类型的 Schema(Swagger 模型定义) + var schema = context.SchemaGenerator.GenerateSchema( + parameter.Type, + context.SchemaRepository); + + // 将参数添加到 Request Body + operation.RequestBody = new OpenApiRequestBody + { + Content = new Dictionary + { + { + "application/json", // 假设使用 JSON 格式 + new OpenApiMediaType { Schema = schema } + } + }, + Description = "内容为加密前内容,需要转换为JSON后使用AES加密后提交,提交格式为 {\"data\":\"AES密文\"}", + Required = true // 标记为必填 + }; + } + } +} \ No newline at end of file diff --git a/Helpers/SimApiAuthOperationFilter.cs b/SwaggerFilters/SimApiAuthOperationFilter.cs similarity index 97% rename from Helpers/SimApiAuthOperationFilter.cs rename to SwaggerFilters/SimApiAuthOperationFilter.cs index 362c21b..4744e29 100644 --- a/Helpers/SimApiAuthOperationFilter.cs +++ b/SwaggerFilters/SimApiAuthOperationFilter.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -6,7 +5,7 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using SimApi.Attributes; -namespace SimApi.Helpers +namespace SimApi.SwaggerFilters { public class SimApiAuthOperationFilter : IOperationFilter { diff --git a/Helpers/SimApiResponseSchemaFilter.cs b/SwaggerFilters/SimApiResponseOperationFilter.cs similarity index 97% rename from Helpers/SimApiResponseSchemaFilter.cs rename to SwaggerFilters/SimApiResponseOperationFilter.cs index 8a9d2d4..d0056ea 100644 --- a/Helpers/SimApiResponseSchemaFilter.cs +++ b/SwaggerFilters/SimApiResponseOperationFilter.cs @@ -9,9 +9,9 @@ using Microsoft.AspNetCore.Mvc; using SimApi.Attributes; using SimApi.Communications; -namespace SimApi.Helpers; +namespace SimApi.SwaggerFilters; -public class SimApiResponseSchemaFilter : IOperationFilter +public class SimApiResponseOperationFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { diff --git a/SwaggerFilters/SimApiSignOperationFilter.cs b/SwaggerFilters/SimApiSignOperationFilter.cs new file mode 100644 index 0000000..e584d79 --- /dev/null +++ b/SwaggerFilters/SimApiSignOperationFilter.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using SimApi.Attributes; +using SimApi.ModelBinders; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace SimApi.SwaggerFilters; + +public class SimApiSignOperationFilter(IServiceProvider serviceProvider) : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // 1. 检查当前方法或类是否标注了 SimApiSignAttribute 及其子类 + var signAttribute = context.MethodInfo.GetCustomAttribute(inherit: true) + ?? context.MethodInfo.DeclaringType?.GetCustomAttribute(inherit: true); + + if (signAttribute == null) + { + return; + } + + var keyProviderType = signAttribute.KeyProvider; + if (!typeof(SimApiSignProviderBase).IsAssignableFrom(keyProviderType)) + { + throw new InvalidOperationException($"KeyProvider 必须继承自 {nameof(SimApiSignProviderBase)}"); + } + + SimApiSignProviderBase? keyProvider; + try + { + keyProvider = + serviceProvider.CreateScope().ServiceProvider.GetService(keyProviderType) as SimApiSignProviderBase; + } + catch (Exception ex) + { + throw new InvalidOperationException($"无法从 DI 容器获取 {keyProviderType.Name} 实例:{ex.Message}"); + } + + if (keyProvider == null) + { + throw new InvalidOperationException($"{keyProviderType.Name} 未在 DI 容器中注册"); + } + + var signStr = keyProvider.SignFields.Aggregate(string.Empty, (current, field) => current + $"{field}=xxx&"); + if (!string.IsNullOrEmpty(keyProvider.AppIdName)) + { + signStr += $"{keyProvider.AppIdName}=xxx&"; + } + + signStr += $"{keyProvider.TimestampName}=xxx&{keyProvider.NonceName}=xxx&签名密钥"; + + var signParameters = new List<(string Name, string Description, bool Required)> + { + (keyProvider.TimestampName, "时间戳(秒级)", true), + (keyProvider.NonceName, "随机字符串", true), + (keyProvider.SignName, $"MD5签名结果,签名MD5字符串: {signStr}", true) + }; + if (keyProvider.AppIdName != null) + { + signParameters.Add((keyProvider.AppIdName, "应用标识", true)); + } + + signParameters.AddRange(keyProvider.SignFields.Where(x => x != keyProvider.AppIdName) + .Select(f => (f, string.Empty, true))); + + foreach (var (name, description, required) in signParameters) + { + if (operation.Parameters?.Any(p => p.Name == name) == true) + { + var tmp = operation.Parameters?.FirstOrDefault(p => p.Name == name); + if (tmp != null) + { + tmp.Required = required; + tmp.Description = description; + } + continue; + } + + operation.Parameters ??= new List(); + operation.Parameters.Add(new OpenApiParameter + { + Name = name, + In = ParameterLocation.Query, // 指定为 Query 参数 + Description = description, + Required = required, + Schema = new OpenApiSchema + { + Type = "string" // 签名相关参数通常为字符串类型 + } + }); + } + } +} \ No newline at end of file