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]
|
[requires]
|
||||||
soulsoft_extensions_hosting = {version = "1.0.20260528"}
|
soulsoft_extensions_hosting = {version = "1.0.20260528"}
|
||||||
soulsoft_web_http = {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_extensions_options_configuration = {version = "1.0.20260528"}
|
||||||
soulsoft_web_mvc = {version = "1.0.20260528"}
|
|
||||||
soulsoft_extensions_options = {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_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_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"}
|
redis = {version = "1.0.20260627"}
|
||||||
soulsoft_extensions_logging_configuration = {version = "1.0.20260528"}
|
soulsoft_identity_claims = {version = "1.0.20260528"}
|
||||||
soulsoft_extensions_injection = {version = "1.0.20260528"}
|
|
||||||
soulsoft_extensions_configuration = {version = "1.0.20260528"}
|
|
||||||
soulsoft_extensions_logging = {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_options.*
|
||||||
import soulsoft_extensions_injection.*
|
import soulsoft_extensions_injection.*
|
||||||
import simapi_serialization.*
|
import simapi_serialization.*
|
||||||
import simapi.annotations.{SimApiAuth as SimApiAuthAttribute, OriginResponse}
|
import simapi.annotations.{SimApiAuth as SimApiAuthAttribute, OriginResponse, SimApiSign, AesBody}
|
||||||
import simapi.communications.*
|
import simapi.communications.*
|
||||||
import simapi.configurations.*
|
import simapi.configurations.*
|
||||||
import simapi.interfaces.*
|
import simapi.interfaces.*
|
||||||
@@ -63,6 +63,7 @@ struct SimApiActionInvoker {
|
|||||||
public func apply(): Unit {
|
public func apply(): Unit {
|
||||||
let controller = createControllerInstance()
|
let controller = createControllerInstance()
|
||||||
checkSimApiAuth()
|
checkSimApiAuth()
|
||||||
|
checkSimApiSign()
|
||||||
// 预读并缓存请求体(body 流不可重读;若请求日志中间件已读,直接用其缓存)
|
// 预读并缓存请求体(body 流不可重读;若请求日志中间件已读,直接用其缓存)
|
||||||
if (!context.items.contains(BODY_CACHE_KEY)) {
|
if (!context.items.contains(BODY_CACHE_KEY)) {
|
||||||
context.items[BODY_CACHE_KEY] = readBody()
|
context.items[BODY_CACHE_KEY] = readBody()
|
||||||
@@ -102,6 +103,9 @@ struct SimApiActionInvoker {
|
|||||||
if (isExplicitlyBound(parameter)) {
|
if (isExplicitlyBound(parameter)) {
|
||||||
// Query/Form/Route/Header/Services → soulsoft
|
// Query/Form/Route/Header/Services → soulsoft
|
||||||
bound[index] = soulsoftBound[index]
|
bound[index] = soulsoftBound[index]
|
||||||
|
} else if (let Some(aes) <- parameter.findAnnotation<AesBody>()) {
|
||||||
|
// @AesBody → 解密 body 后按参数类型反序列化(对齐 C# AesBodyModelBinder)
|
||||||
|
bound[index] = bindAesBody(context, parameter, aes)
|
||||||
} else {
|
} else {
|
||||||
// FromBody → simapi_serialization 按运行时类型反序列化(免 @Serialization 宏)
|
// FromBody → simapi_serialization 按运行时类型反序列化(免 @Serialization 宏)
|
||||||
bound[index] = bindFromBody(context, parameter)
|
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 {
|
private func readBody(): String {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user