feat: 新增 @SimApiSign 与 @AesBody 注解(对齐 C# SimApiSignAttribute / AesBodyAttribute)

- 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# 行为)
This commit is contained in:
2026-08-18 00:35:59 +08:00
parent d76b08a542
commit ef9765de5d
4 changed files with 158 additions and 10 deletions
+9 -9
View File
@@ -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"}
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 声明式 AES body 解密注解(对齐 C# SimApi.Attributes.AesBodyAttribute)。
*
* 标注在控制器方法参数上,请求派发时(SimApiRequestDelegateFactory)自动解密并反序列化:
* - 读取请求体 {"data": "密文"}
* - 通过 keyProviderDI 解析)获取密钥
* - 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
}
}
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 声明式签名校验注解(对齐 C# SimApi.Attributes.SimApiSignAttribute)。
*
* 标注在控制器方法或类上,请求派发时(SimApiRequestDelegateFactory)自动执行验签:
* - 提取 appId / timestamp / nonce / signQuery 优先,其次 Header
* - 通过 keyProviderDI 解析)获取密钥
* - 过期校验 + nonce 去重(需缓存)
* - 拼接 SignFields + appId + timestamp + nonce + keyMD5 比对
*
* 用法:
* @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
}
}
+82 -1
View File
@@ -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>()) {
// @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 解析 keyProviderAesBodyProviderBase 实现)
let provider = resolveAesProvider(aes.keyProvider)
// 2. 读取并解密 bodySimApiAesBodyChecker.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 解析 keyProviderSimApiSignProviderBase 实现)
let provider = resolveSignProvider(sign.keyProvider)
// 2. 解析缓存(nonce 去重;DI 有 SimApiCache 则用)
var cache: ?SimApiCache = None
try {
cache = Some(context.services.getOrThrow<SimApiCache>())
} 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 {