Compare commits

...
2 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
8 changed files with 352 additions and 43 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, "签名错误");
}
}
}
+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 Microsoft.AspNetCore.Mvc;
using SimApi.Attributes;
using SimApi.Communications; using SimApi.Communications;
using SimApi.Helpers; using SimApi.Helpers;
namespace SimApi.Controllers; namespace SimApi.Controllers;
public class SimApiCommonController(SimApiAuth auth) : SimApiBaseController public class SimApiCommonController : SimApiBaseController
{ {
/// <summary> /// <summary>
/// 错误回馈页面 /// 错误回馈页面
@@ -20,37 +19,6 @@ public class SimApiCommonController(SimApiAuth auth) : SimApiBaseController
return new SimApiBaseResponse(code); 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] [HttpPost, HttpGet]
public SimApiBaseResponse<Dictionary<string, string>> Versions() 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);
}
+4 -3
View File
@@ -141,6 +141,7 @@ public static class SimApiExtensions
{ {
return docName == "api"; // 未分组接口只属于默认分组v1 return docName == "api"; // 未分组接口只属于默认分组v1
} }
return docName == actionGroupName; return docName == actionGroupName;
}); });
@@ -324,19 +325,19 @@ public static class SimApiExtensions
builder.MapControllerRoute(name: "GetUserInfo", pattern: "/user/info", builder.MapControllerRoute(name: "GetUserInfo", pattern: "/user/info",
defaults: new defaults: new
{ {
controller = "SimApiCommon", controller = "SimApiAuth",
action = "UserInfo" action = "UserInfo"
}); });
builder.MapControllerRoute(name: "CheckLogin", pattern: "/auth/check", builder.MapControllerRoute(name: "CheckLogin", pattern: "/auth/check",
defaults: new defaults: new
{ {
controller = "SimApiCommon", controller = "SimApiAuth",
action = "CheckLogin" action = "CheckLogin"
}); });
builder.MapControllerRoute(name: "Logout", pattern: "/auth/logout", builder.MapControllerRoute(name: "Logout", pattern: "/auth/logout",
defaults: new defaults: new
{ {
controller = "SimApiCommon", controller = "SimApiAuth",
action = "Logout" action = "Logout"
}); });
if (options.EnableCoceSdk) if (options.EnableCoceSdk)