From ef9765de5dc4ad44fea0d212d671c55ec7cef27f Mon Sep 17 00:00:00 2001 From: xRain Date: Tue, 18 Aug 2026 00:35:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20@SimApiSign=20?= =?UTF-8?q?=E4=B8=8E=20@AesBody=20=E6=B3=A8=E8=A7=A3=EF=BC=88=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=20C#=20SimApiSignAttribute=20/=20AesBodyAttribute?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/annotations/SimApiSign.cj:方法/类级声明式验签注解,keyProvider 用类型名(DI 解析) - src/annotations/AesBody.cj:参数级声明式 AES body 解密注解 - SimApiRequestDelegateFactory 自动执行: - checkSimApiSign:解析 @SimApiSign → DI 取 SimApiSignProviderBase → SimApiSignChecker.verify - bindAesBody:@AesBody 参数 → DI 取 AesBodyProviderBase → decryptBody → 按类型反序列化 - 注解未指定类型名时用默认空实现(未配置即报错,对齐 C# 行为) --- cjpm.lock | 18 ++--- src/annotations/AesBody.cj | 33 ++++++++ src/annotations/SimApiSign.cj | 34 +++++++++ src/helpers/SimApiRequestDelegateFactory.cj | 83 ++++++++++++++++++++- 4 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 src/annotations/AesBody.cj create mode 100644 src/annotations/SimApiSign.cj diff --git a/cjpm.lock b/cjpm.lock index 3f6e068..ebeb7c6 100644 --- a/cjpm.lock +++ b/cjpm.lock @@ -3,18 +3,18 @@ version = 0 [requires] soulsoft_extensions_hosting = {version = "1.0.20260528"} soulsoft_web_http = {version = "1.0.20260528"} - soulsoft_web_hosting = {version = "1.0.20260528"} - soulsoft_web_routing = {version = "1.0.20260528"} soulsoft_extensions_options_configuration = {version = "1.0.20260528"} - soulsoft_web_mvc = {version = "1.0.20260528"} soulsoft_extensions_options = {version = "1.0.20260528"} - soulsoft_web_cors = {version = "1.0.20260528"} + soulsoft_web_routing = {version = "1.0.20260528"} soulsoft_serialization = {version = "1.0.20260528"} - soulsoft_identity_claims = {version = "1.0.20260528"} + soulsoft_web_hosting = {version = "1.0.20260528"} soulsoft_net_http = {version = "1.0.20260528"} - soulsoft_extensions_logging_console = {version = "1.0.20260528"} + soulsoft_web_mvc = {version = "1.0.20260528"} + soulsoft_web_cors = {version = "1.0.20260528"} redis = {version = "1.0.20260627"} - soulsoft_extensions_logging_configuration = {version = "1.0.20260528"} - soulsoft_extensions_injection = {version = "1.0.20260528"} - soulsoft_extensions_configuration = {version = "1.0.20260528"} + soulsoft_identity_claims = {version = "1.0.20260528"} soulsoft_extensions_logging = {version = "1.0.20260528"} + soulsoft_extensions_logging_console = {version = "1.0.20260528"} + soulsoft_extensions_logging_configuration = {version = "1.0.20260528"} + soulsoft_extensions_configuration = {version = "1.0.20260528"} + soulsoft_extensions_injection = {version = "1.0.20260528"} diff --git a/src/annotations/AesBody.cj b/src/annotations/AesBody.cj new file mode 100644 index 0000000..2c1c5d5 --- /dev/null +++ b/src/annotations/AesBody.cj @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 SimcuTeam. All rights reserved. + * 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。 + * 声明式 AES body 解密注解(对齐 C# SimApi.Attributes.AesBodyAttribute)。 + * + * 标注在控制器方法参数上,请求派发时(SimApiRequestDelegateFactory)自动解密并反序列化: + * - 读取请求体 {"data": "密文"} + * - 通过 keyProvider(DI 解析)获取密钥 + * - SimApiAesUtil.decrypt 解密得到明文 JSON + * - simapi_serialization 按参数类型反序列化 + * + * 用法: + * public func create(@AesBody request: CreateRequest): Unit { + * + * 说明:仓颉注解参数须为编译期常量,无法直接持有 Type; + * 故 keyProvider 用类型名 String,运行时经 TypeInfo.get 解析后从 DI 取实例。 + */ + +package simapi.annotations + +@Annotation[target: [Parameter]] +public class AesBody { + /// AES 密钥提供器类型名(DI 注册的 AesBodyProviderBase 实现类名) + public let keyProvider: String + + public const init() { + this.keyProvider = "" + } + + public const init(keyProvider: String) { + this.keyProvider = keyProvider + } +} diff --git a/src/annotations/SimApiSign.cj b/src/annotations/SimApiSign.cj new file mode 100644 index 0000000..d251e7a --- /dev/null +++ b/src/annotations/SimApiSign.cj @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 SimcuTeam. All rights reserved. + * 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。 + * 声明式签名校验注解(对齐 C# SimApi.Attributes.SimApiSignAttribute)。 + * + * 标注在控制器方法或类上,请求派发时(SimApiRequestDelegateFactory)自动执行验签: + * - 提取 appId / timestamp / nonce / sign(Query 优先,其次 Header) + * - 通过 keyProvider(DI 解析)获取密钥 + * - 过期校验 + nonce 去重(需缓存) + * - 拼接 SignFields + appId + timestamp + nonce + key,MD5 比对 + * + * 用法: + * @SimApiSign // 默认 SimApiSignProviderBase(应用需注册实现) + * @SimApiSign["MySignProvider"] // 指定 provider 类型名(DI 注册的实现类) + * + * 说明:仓颉注解参数须为编译期常量,无法直接持有 Type; + * 故 keyProvider 用类型名 String,运行时经 TypeInfo.get 解析后从 DI 取实例。 + */ + +package simapi.annotations + +@Annotation[target: [MemberFunction, Type]] +public class SimApiSign { + /// 签名提供器类型名(DI 注册的 SimApiSignProviderBase 实现类名) + public let keyProvider: String + + public const init() { + this.keyProvider = "" + } + + public const init(keyProvider: String) { + this.keyProvider = keyProvider + } +} diff --git a/src/helpers/SimApiRequestDelegateFactory.cj b/src/helpers/SimApiRequestDelegateFactory.cj index 2c54fff..e728e59 100644 --- a/src/helpers/SimApiRequestDelegateFactory.cj +++ b/src/helpers/SimApiRequestDelegateFactory.cj @@ -26,7 +26,7 @@ import soulsoft_web_mvc.abstractions.* import soulsoft_extensions_options.* import soulsoft_extensions_injection.* import simapi_serialization.* -import simapi.annotations.{SimApiAuth as SimApiAuthAttribute, OriginResponse} +import simapi.annotations.{SimApiAuth as SimApiAuthAttribute, OriginResponse, SimApiSign, AesBody} import simapi.communications.* import simapi.configurations.* import simapi.interfaces.* @@ -63,6 +63,7 @@ struct SimApiActionInvoker { public func apply(): Unit { let controller = createControllerInstance() checkSimApiAuth() + checkSimApiSign() // 预读并缓存请求体(body 流不可重读;若请求日志中间件已读,直接用其缓存) if (!context.items.contains(BODY_CACHE_KEY)) { context.items[BODY_CACHE_KEY] = readBody() @@ -102,6 +103,9 @@ struct SimApiActionInvoker { if (isExplicitlyBound(parameter)) { // Query/Form/Route/Header/Services → soulsoft bound[index] = soulsoftBound[index] + } else if (let Some(aes) <- parameter.findAnnotation()) { + // @AesBody → 解密 body 后按参数类型反序列化(对齐 C# AesBodyModelBinder) + bound[index] = bindAesBody(context, parameter, aes) } else { // FromBody → simapi_serialization 按运行时类型反序列化(免 @Serialization 宏) bound[index] = bindFromBody(context, parameter) @@ -136,6 +140,83 @@ struct SimApiActionInvoker { () } + /// @AesBody 参数绑定:解密 body 后按参数类型反序列化(对齐 C# AesBodyModelBinder) + private func bindAesBody(context: ActionBindingContext, parameter: ParameterInfo, aes: AesBody): Any { + // 1. 从 DI 解析 keyProvider(AesBodyProviderBase 实现) + let provider = resolveAesProvider(aes.keyProvider) + // 2. 读取并解密 body(SimApiAesBodyChecker.decryptBody 内部读取原始 body 流) + let plain = SimApiAesBodyChecker.decryptBody(context.httpContext, provider) + // 3. 按参数类型反序列化明文 JSON + try { + return JsonSerializer.Deserialize(parameter.typeInfo, plain) + } catch (ex: Exception) { + SimApiError.error(code: 400, message: "AES body 反序列化失败: ${ex.message}") + } + () + } + + /// 从 DI 解析 AesBodyProviderBase 实现(注解未指定类型名时返回默认空实现) + private func resolveAesProvider(keyProvider: String): AesBodyProviderBase { + if (keyProvider.isEmpty()) { + return AesBodyProviderBase() + } + var typeInfo: ?TypeInfo = None + try { + typeInfo = Some(TypeInfo.get(keyProvider)) + } catch (_: Exception) { + SimApiError.error(code: 400, message: "未找到 AES 密钥提供器 ${keyProvider}") + } + let instance = context.services.getOrThrow(typeInfo.getOrThrow()) + if (let p: AesBodyProviderBase <- instance) { + return p + } + SimApiError.error(code: 400, message: "密钥提供器 ${keyProvider} 未实现 AesBodyProviderBase") + AesBodyProviderBase() + } + + /// 检查 @SimApiSign 注解并执行验签(对齐 C# SimApiSignAttribute.OnActionExecuting) + private func checkSimApiSign() { + var sign: ?SimApiSign = None + for (item in actionDescriptor.endpointMetadata) { + if (let s: SimApiSign <- item) { + sign = Some(s) + break + } + } + if (let Some(sign) <- sign) { + // 1. 从 DI 解析 keyProvider(SimApiSignProviderBase 实现) + let provider = resolveSignProvider(sign.keyProvider) + // 2. 解析缓存(nonce 去重;DI 有 SimApiCache 则用) + var cache: ?SimApiCache = None + try { + cache = Some(context.services.getOrThrow()) + } catch (_: Exception) { + // 未注册缓存 → 跳过 nonce 去重 + } + // 3. 执行验签 + SimApiSignChecker.verify(context, provider, cache) + } + } + + /// 从 DI 解析 SimApiSignProviderBase 实现(注解未指定类型名时返回默认空实现) + private func resolveSignProvider(keyProvider: String): SimApiSignProviderBase { + if (keyProvider.isEmpty()) { + return SimApiSignProviderBase() + } + var typeInfo: ?TypeInfo = None + try { + typeInfo = Some(TypeInfo.get(keyProvider)) + } catch (_: Exception) { + SimApiError.error(code: 400, message: "未找到签名提供器 ${keyProvider}") + } + let instance = context.services.getOrThrow(typeInfo.getOrThrow()) + if (let p: SimApiSignProviderBase <- instance) { + return p + } + SimApiError.error(code: 400, message: "签名提供器 ${keyProvider} 未实现 SimApiSignProviderBase") + SimApiSignProviderBase() + } + /// 读取并重置请求体流(供后续业务读取) private func readBody(): String { try {