Skip to content

Commit 65451d7

Browse files
committed
fix swagger, add simapihttpclient
1 parent c7373da commit 65451d7

10 files changed

Lines changed: 243 additions & 14 deletions

Attributes/AesBodyAttribute.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.AspNetCore.Mvc.ModelBinding;
34
using SimApi.ModelBinders;
45

56
namespace SimApi.Attributes;
@@ -8,6 +9,7 @@ namespace SimApi.Attributes;
89
public class AesBodyAttribute : ModelBinderAttribute
910
{
1011
public Type KeyProvider { get; set; } = typeof(AesBodyProviderBase);
12+
public override BindingSource BindingSource => BindingSource.Body;
1113

1214
public AesBodyAttribute()
1315
{

Attributes/SimApiSignAttribute.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace SimApi.Attributes;
1212
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
1313
public class SimApiSignAttribute : ActionFilterAttribute
1414
{
15-
protected Type KeyProvider { get; set; } = typeof(SimApiSignProviderBase);
15+
public Type KeyProvider { get; set; } = typeof(SimApiSignProviderBase);
1616

1717
public override void OnActionExecuting(ActionExecutingContext context)
1818
{
@@ -101,6 +101,11 @@ public override void OnActionExecuting(ActionExecutingContext context)
101101
signStr += "&";
102102
}
103103

104+
if (!string.IsNullOrEmpty(keyProvider.AppIdName))
105+
{
106+
signStr += $"{keyProvider.AppIdName}={appId}&";
107+
}
108+
104109
signStr += $"{keyProvider.TimestampName}={ts}&{keyProvider.NonceName}={nonce}&{key}";
105110
var sign = context.HttpContext.Request.Query[keyProvider.SignName]
106111
.FirstOrDefault() ?? context.HttpContext.Request.Headers[keyProvider.SignName]

Helpers/SimApiHttpClient.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net.Http;
5+
using System.Net.Http.Json;
6+
using SimApi.Communications;
7+
8+
namespace SimApi.Helpers;
9+
10+
public class SimApiHttpClient(string? appId, string appKey)
11+
{
12+
public string SignName { get; init; } = "sign";
13+
public string TimestampName { get; init; } = "timestamp";
14+
public string NonceName { get; init; } = "nonce";
15+
public string? AppIdName { get; init; } = "appId";
16+
public string[] SignFields { get; init; } = [];
17+
18+
19+
public T? SignQuery<T>(string url, object body, Dictionary<string, string>? queries = null)
20+
{
21+
var queryUrl = SignFields.Aggregate(string.Empty,
22+
(current, signField) => current + $"{signField}={queries?[signField]}&");
23+
if (!string.IsNullOrEmpty(AppIdName))
24+
{
25+
queryUrl += $"{AppIdName}={appId}&";
26+
}
27+
28+
queryUrl += $"{TimestampName}={(int)SimApiUtil.TimestampNow}&{NonceName}={Guid.NewGuid()}";
29+
var signStr = $"{queryUrl}&{appKey}";
30+
var path = $"{url}?{queryUrl}&{SignName}={SimApiUtil.Md5(signStr)}";
31+
32+
if (queries != null)
33+
{
34+
path = queries.Where(q => !SignFields.Contains(q.Key))
35+
.Aggregate(path, (current, q) => current + $"&{q.Key}={q.Value}");
36+
}
37+
38+
var http = new HttpClient();
39+
var resp = http.PostAsJsonAsync(path, body).Result;
40+
return resp.Content.ReadFromJsonAsync<T>().Result;
41+
}
42+
43+
public T? AesQuery<T>(string url, object body)
44+
{
45+
if (!string.IsNullOrEmpty(AppIdName))
46+
{
47+
url += $"?{AppIdName}={appId}";
48+
}
49+
50+
var req = new SimApiOneFieldRequest<string>
51+
{
52+
Data = SimApiAesUtil.Encrypt(SimApiUtil.Json(body), appKey)
53+
};
54+
var http = new HttpClient();
55+
var resp = http.PostAsJsonAsync(url, req).Result;
56+
return resp.Content.ReadFromJsonAsync<T>().Result;
57+
}
58+
59+
public T? AesSignQuery<T>(string url, object body, Dictionary<string, string>? queries = null)
60+
{
61+
var req = new SimApiOneFieldRequest<string>
62+
{
63+
Data = SimApiAesUtil.Encrypt(SimApiUtil.Json(body), appKey)
64+
};
65+
return SignQuery<T>(url, req, queries);
66+
}
67+
}

ModelBinders/AesBodyProviderBase.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
using SimApi.Exceptions;
2+
13
namespace SimApi.ModelBinders;
24

35
/// <summary>
46
/// 密钥提供器接口(抽象密钥获取逻辑)
57
/// </summary>
68
public abstract class AesBodyProviderBase
79
{
8-
public string? AppIdName { get; set; } = "appId";
10+
public virtual string? AppIdName { get; set; } = "appId";
911

1012

1113
/// <summary>

ModelBinders/SimApiSignProviderBase.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,40 @@
1+
using SimApi.Exceptions;
2+
13
namespace SimApi.ModelBinders;
24

35
public abstract class SimApiSignProviderBase
46
{
57
/// <summary>
68
/// appId字段的名称
79
/// </summary>
8-
public string? AppIdName { get; set; } = "appId";
10+
public virtual string? AppIdName { get; set; } = "appId";
911

1012
/// <summary>
1113
/// 时间戳的字段名
1214
/// </summary>
13-
public string TimestampName { get; set; } = "timestamp";
15+
public virtual string TimestampName { get; set; } = "timestamp";
1416

1517
/// <summary>
1618
/// 随机字符串的字段名
1719
/// </summary>
18-
public string NonceName { get; set; } = "nonce";
20+
public virtual string NonceName { get; set; } = "nonce";
1921

2022
/// <summary>
2123
/// 签名的字段名
2224
/// </summary>
23-
public string SignName { get; set; } = "sign";
25+
public virtual string SignName { get; set; } = "sign";
2426

2527
/// <summary>
2628
/// 请求过期时间, 如果为0, 不校验timestamp
2729
/// </summary>
28-
public int QueryExpires { get; set; } = 5;
30+
public virtual int QueryExpires { get; set; } = 5;
2931

3032
/// <summary>
3133
/// 如果开启,必须配置redis, 每次请求将会缓存nonce
3234
/// </summary>
33-
public bool DuplicateRequestProtection { get; set; } = true;
35+
public virtual bool DuplicateRequestProtection { get; set; } = true;
3436

35-
public string[] SignFields { get; set; } = ["appId"];
37+
public virtual string[] SignFields { get; set; } = [];
3638

3739
/// <summary>
3840
/// 根据appId获取对应的密钥

SimApiExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using SimApi.CoceSdk;
2020
using SimApi.Configurations;
2121
using SimApi.Logger;
22+
using SimApi.SwaggerFilters;
2223

2324
namespace SimApi;
2425

@@ -123,7 +124,9 @@ public static IServiceCollection AddSimApi(this IServiceCollection builder,
123124
}
124125

125126
x.CustomSchemaIds(type => type.FullName?.Replace("+", "."));
126-
x.OperationFilter<SimApiResponseSchemaFilter>();
127+
x.OperationFilter<SimApiResponseOperationFilter>();
128+
x.OperationFilter<SimApiSignOperationFilter>();
129+
x.OperationFilter<AesBodyOperationFilter>();
127130
if (simApiOptions.EnableSimApiAuth)
128131
{
129132
x.OperationFilter<SimApiAuthOperationFilter>();
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using SimApi.Attributes;
4+
5+
namespace SimApi.SwaggerFilters;
6+
7+
using Microsoft.OpenApi.Models;
8+
using Swashbuckle.AspNetCore.SwaggerGen;
9+
using System.Reflection;
10+
11+
/// <summary>
12+
/// 自定义 Swagger 过滤器:将标注 [AesBody] 的参数显示在 Request Body 中
13+
/// </summary>
14+
public class AesBodyOperationFilter : IOperationFilter
15+
{
16+
public void Apply(OpenApiOperation operation, OperationFilterContext context)
17+
{
18+
foreach (var parameter in context.ApiDescription.ParameterDescriptions)
19+
{
20+
var hasAesBodyAttr = parameter.ParameterInfo()
21+
.GetCustomAttribute<AesBodyAttribute>() != null;
22+
if (!hasAesBodyAttr) continue;
23+
// 1. 移除默认的 Query 参数描述(如果存在)
24+
var queryParam = operation.Parameters
25+
.FirstOrDefault(p => p.Name == parameter.Name);
26+
if (queryParam != null)
27+
{
28+
operation.Parameters.Remove(queryParam);
29+
}
30+
31+
// 2. 添加 Body 参数描述
32+
// 获取参数类型的 Schema(Swagger 模型定义)
33+
var schema = context.SchemaGenerator.GenerateSchema(
34+
parameter.Type,
35+
context.SchemaRepository);
36+
37+
// 将参数添加到 Request Body
38+
operation.RequestBody = new OpenApiRequestBody
39+
{
40+
Content = new Dictionary<string, OpenApiMediaType>
41+
{
42+
{
43+
"application/json", // 假设使用 JSON 格式
44+
new OpenApiMediaType { Schema = schema }
45+
}
46+
},
47+
Description = "内容为加密前内容,需要转换为JSON后使用AES加密后提交,提交格式为 {\"data\":\"AES密文\"}",
48+
Required = true // 标记为必填
49+
};
50+
}
51+
}
52+
}

Helpers/SimApiAuthOperationFilter.cs renamed to SwaggerFilters/SimApiAuthOperationFilter.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
using System;
21
using System.Collections.Generic;
32
using System.Linq;
43
using System.Reflection;
54
using Microsoft.OpenApi.Models;
65
using Swashbuckle.AspNetCore.SwaggerGen;
76
using SimApi.Attributes;
87

9-
namespace SimApi.Helpers
8+
namespace SimApi.SwaggerFilters
109
{
1110
public class SimApiAuthOperationFilter : IOperationFilter
1211
{

Helpers/SimApiResponseSchemaFilter.cs renamed to SwaggerFilters/SimApiResponseOperationFilter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
using SimApi.Attributes;
1010
using SimApi.Communications;
1111

12-
namespace SimApi.Helpers;
12+
namespace SimApi.SwaggerFilters;
1313

14-
public class SimApiResponseSchemaFilter : IOperationFilter
14+
public class SimApiResponseOperationFilter : IOperationFilter
1515
{
1616
public void Apply(OpenApiOperation operation, OperationFilterContext context)
1717
{
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.OpenApi.Models;
7+
using SimApi.Attributes;
8+
using SimApi.ModelBinders;
9+
using Swashbuckle.AspNetCore.SwaggerGen;
10+
11+
namespace SimApi.SwaggerFilters;
12+
13+
public class SimApiSignOperationFilter(IServiceProvider serviceProvider) : IOperationFilter
14+
{
15+
public void Apply(OpenApiOperation operation, OperationFilterContext context)
16+
{
17+
// 1. 检查当前方法或类是否标注了 SimApiSignAttribute 及其子类
18+
var signAttribute = context.MethodInfo.GetCustomAttribute<SimApiSignAttribute>(inherit: true)
19+
?? context.MethodInfo.DeclaringType?.GetCustomAttribute<SimApiSignAttribute>(inherit: true);
20+
21+
if (signAttribute == null)
22+
{
23+
return;
24+
}
25+
26+
var keyProviderType = signAttribute.KeyProvider;
27+
if (!typeof(SimApiSignProviderBase).IsAssignableFrom(keyProviderType))
28+
{
29+
throw new InvalidOperationException($"KeyProvider 必须继承自 {nameof(SimApiSignProviderBase)}");
30+
}
31+
32+
SimApiSignProviderBase? keyProvider;
33+
try
34+
{
35+
keyProvider =
36+
serviceProvider.CreateScope().ServiceProvider.GetService(keyProviderType) as SimApiSignProviderBase;
37+
}
38+
catch (Exception ex)
39+
{
40+
throw new InvalidOperationException($"无法从 DI 容器获取 {keyProviderType.Name} 实例:{ex.Message}");
41+
}
42+
43+
if (keyProvider == null)
44+
{
45+
throw new InvalidOperationException($"{keyProviderType.Name} 未在 DI 容器中注册");
46+
}
47+
48+
var signStr = keyProvider.SignFields.Aggregate(string.Empty, (current, field) => current + $"{field}=xxx&");
49+
if (!string.IsNullOrEmpty(keyProvider.AppIdName))
50+
{
51+
signStr += $"{keyProvider.AppIdName}=xxx&";
52+
}
53+
54+
signStr += $"{keyProvider.TimestampName}=xxx&{keyProvider.NonceName}=xxx&签名密钥";
55+
56+
var signParameters = new List<(string Name, string Description, bool Required)>
57+
{
58+
(keyProvider.TimestampName, "时间戳(秒级)", true),
59+
(keyProvider.NonceName, "随机字符串", true),
60+
(keyProvider.SignName, $"MD5签名结果,签名MD5字符串: {signStr}", true)
61+
};
62+
if (keyProvider.AppIdName != null)
63+
{
64+
signParameters.Add((keyProvider.AppIdName, "应用标识", true));
65+
}
66+
67+
signParameters.AddRange(keyProvider.SignFields.Where(x => x != keyProvider.AppIdName)
68+
.Select(f => (f, string.Empty, true)));
69+
70+
foreach (var (name, description, required) in signParameters)
71+
{
72+
if (operation.Parameters?.Any(p => p.Name == name) == true)
73+
{
74+
var tmp = operation.Parameters?.FirstOrDefault(p => p.Name == name);
75+
if (tmp != null)
76+
{
77+
tmp.Required = required;
78+
tmp.Description = description;
79+
}
80+
continue;
81+
}
82+
83+
operation.Parameters ??= new List<OpenApiParameter>();
84+
operation.Parameters.Add(new OpenApiParameter
85+
{
86+
Name = name,
87+
In = ParameterLocation.Query, // 指定为 Query 参数
88+
Description = description,
89+
Required = required,
90+
Schema = new OpenApiSchema
91+
{
92+
Type = "string" // 签名相关参数通常为字符串类型
93+
}
94+
});
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)