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:
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 解析 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<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 {
|
||||
|
||||
Reference in New Issue
Block a user