feat: 自定义 FromBody 绑定——simapi_serialization 按运行时类型反序列化,DTO 免 @Serialization 宏
- SimApiRequestDelegateFactory:无注解参数(FromBody)用 JsonSerializer.Deserialize(typeInfo, body) 反序列化(免宏、免约束),显式注解参数(Query/Form/Route/Header/Services)委托 soulsoft binder - 请求体只读一次并缓存到 context.items["SimApi:BodyCache"](body 流不可重读, 请求日志中间件与 FromBody 共用缓存) - 验证:/auth/login/admin 的 LoginAdminRequest(无宏)反序列化成功,走到业务密码校验
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
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_hosting = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_options_configuration = {version = "1.0.20260528"}
|
||||
soulsoft_serialization = {version = "1.0.20260528"}
|
||||
soulsoft_web_mvc = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_options = {version = "1.0.20260528"}
|
||||
redis = {version = "1.0.20260627"}
|
||||
soulsoft_web_routing = {version = "1.0.20260528"}
|
||||
soulsoft_net_http = {version = "1.0.20260528"}
|
||||
soulsoft_web_hosting = {version = "1.0.20260528"}
|
||||
soulsoft_web_cors = {version = "1.0.20260528"}
|
||||
soulsoft_identity_claims = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_logging_console = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_logging = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_logging_configuration = {version = "1.0.20260528"}
|
||||
soulsoft_serialization = {version = "1.0.20260528"}
|
||||
redis = {version = "1.0.20260627"}
|
||||
soulsoft_extensions_configuration = {version = "1.0.20260528"}
|
||||
soulsoft_net_http = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_logging_console = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_options = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_injection = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_logging = {version = "1.0.20260528"}
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
package simapi.helpers
|
||||
|
||||
import std.collection.*
|
||||
import std.io.*
|
||||
import std.reflect.*
|
||||
import soulsoft_web_http.*
|
||||
import soulsoft_web_mvc.annotations.*
|
||||
import soulsoft_web_mvc.core.*
|
||||
import soulsoft_web_mvc.routing.*
|
||||
import soulsoft_web_mvc.controllers.*
|
||||
@@ -23,6 +25,7 @@ import soulsoft_web_mvc.modelBindings.*
|
||||
import soulsoft_web_mvc.abstractions.*
|
||||
import soulsoft_extensions_options.*
|
||||
import soulsoft_extensions_injection.*
|
||||
import simapi_serialization.*
|
||||
import simapi.attributes.{SimApiAuth as SimApiAuthAttribute, OriginResponse}
|
||||
import simapi.communications.*
|
||||
import simapi.configurations.*
|
||||
@@ -60,8 +63,14 @@ struct SimApiActionInvoker {
|
||||
public func apply(): Unit {
|
||||
let controller = createControllerInstance()
|
||||
checkSimApiAuth()
|
||||
// 预读并缓存请求体(body 流不可重读;若请求日志中间件已读,直接用其缓存)
|
||||
if (!context.items.contains(BODY_CACHE_KEY)) {
|
||||
context.items[BODY_CACHE_KEY] = readBody()
|
||||
}
|
||||
let modelBindingContext = ActionBindingContext(context, actionDescriptor.actionFunction.parameters)
|
||||
let boundParameters = modelBinder.bind(modelBindingContext)
|
||||
// 自定义绑定:无注解参数(FromBody)用 simapi_serialization 反序列化,
|
||||
// 显式注解参数(Query/Form/Route/Header/Services)委托 soulsoft binder。
|
||||
let boundParameters = bindParameters(modelBindingContext)
|
||||
if (!modelBindingContext.modelState.isValid) {
|
||||
handleInvalidModelState(modelBindingContext)
|
||||
} else {
|
||||
@@ -70,6 +79,85 @@ struct SimApiActionInvoker {
|
||||
}
|
||||
}
|
||||
|
||||
/// 请求体缓存键(body 流不可重读,读一次后缓存)
|
||||
private static let BODY_CACHE_KEY = "SimApi:BodyCache"
|
||||
|
||||
/// 绑定全部参数:FromBody 自实现(simapi_serialization),其余委托 soulsoft
|
||||
private func bindParameters(context: ActionBindingContext): Array<Any> {
|
||||
let params = context.parameters
|
||||
if (params.size == 0) {
|
||||
return []
|
||||
}
|
||||
let bound = Array<Any>(params.size, repeat: ())
|
||||
// 存在显式参数时才委托 soulsoft 绑定(Query/Form/Route/Header/Services)
|
||||
var hasExplicit = false
|
||||
for (parameter in params) {
|
||||
if (isExplicitlyBound(parameter)) {
|
||||
hasExplicit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
let soulsoftBound = if (hasExplicit) { modelBinder.bind(context) } else { Array<Any>(params.size, repeat: ()) }
|
||||
for ((index, parameter) in params |> enumerate) {
|
||||
if (isExplicitlyBound(parameter)) {
|
||||
// Query/Form/Route/Header/Services → soulsoft
|
||||
bound[index] = soulsoftBound[index]
|
||||
} else {
|
||||
// FromBody → simapi_serialization 按运行时类型反序列化(免 @Serialization 宏)
|
||||
bound[index] = bindFromBody(context, parameter)
|
||||
}
|
||||
}
|
||||
bound
|
||||
}
|
||||
|
||||
/// 参数是否显式指定绑定源(FromQuery/FromForm/FromRoute/FromHeader/FromServices)
|
||||
private func isExplicitlyBound(parameter: ParameterInfo): Bool {
|
||||
parameter.findAnnotation<FromQuery>().isSome() ||
|
||||
parameter.findAnnotation<FromForm>().isSome() ||
|
||||
parameter.findAnnotation<FromRoute>().isSome() ||
|
||||
parameter.findAnnotation<FromHeader>().isSome() ||
|
||||
parameter.findAnnotation<FromServices>().isSome()
|
||||
}
|
||||
|
||||
/// 从请求体反序列化(simapi_serialization 按运行时类型,DTO 免标注)
|
||||
private func bindFromBody(context: ActionBindingContext, parameter: ParameterInfo): Any {
|
||||
let body = match (context.httpContext.items.get(BODY_CACHE_KEY)) {
|
||||
case Some(v) => if (let s: String <- v) { s } else { "" }
|
||||
case None => ""
|
||||
}
|
||||
if (body.isEmpty()) {
|
||||
SimApiError.error(code: 400, message: "请求体不能为空")
|
||||
}
|
||||
try {
|
||||
return JsonSerializer.Deserialize(parameter.typeInfo, body)
|
||||
} catch (ex: Exception) {
|
||||
SimApiError.error(code: 400, message: "请求体反序列化失败: ${ex.message}")
|
||||
}
|
||||
()
|
||||
}
|
||||
|
||||
/// 读取并重置请求体流(供后续业务读取)
|
||||
private func readBody(): String {
|
||||
try {
|
||||
context.request.enableBuffering()
|
||||
var buffer = Array<Byte>(4096, repeat: 0)
|
||||
var sb = StringBuilder()
|
||||
var read = context.request.body.read(buffer)
|
||||
while (read > 0) {
|
||||
sb.appendFromUtf8(buffer.slice(0, read))
|
||||
read = context.request.body.read(buffer)
|
||||
}
|
||||
let text = sb.toString()
|
||||
if (let seekable: Seekable <- context.request.body) {
|
||||
seekable.seek(SeekPosition.Begin(0))
|
||||
}
|
||||
text
|
||||
} catch (ex: Exception) {
|
||||
SimApiError.error(code: 400, message: "读取请求体失败: ${ex.message}")
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
/// 检查 @SimApiAuth 注解并执行鉴权(对齐 C# SimApiAuthAttribute.OnActionExecuting):
|
||||
/// 未登录 401 → 类型权限 403 → 遍历执行 ISimApiAuthChecker
|
||||
private func checkSimApiAuth() {
|
||||
|
||||
@@ -122,10 +122,11 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
|
||||
read = context.request.body.read(buffer)
|
||||
}
|
||||
let bodyText = sb.toString()
|
||||
// 重置流位置,供后续业务读取
|
||||
// 重置流位置,供后续业务读取;同时缓存 body(流可能不可重读)
|
||||
if (let seekable: Seekable <- context.request.body) {
|
||||
seekable.seek(SeekPosition.Begin(0))
|
||||
}
|
||||
context.items["SimApi:BodyCache"] = bodyText
|
||||
return truncateBody(bodyText)
|
||||
} catch (_: Exception) {
|
||||
return "(读取请求体失败)\n"
|
||||
|
||||
Reference in New Issue
Block a user