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
|
version = 0
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
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_extensions_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_serialization = {version = "1.0.20260528"}
|
||||||
soulsoft_web_mvc = {version = "1.0.20260528"}
|
soulsoft_web_mvc = {version = "1.0.20260528"}
|
||||||
soulsoft_extensions_options = {version = "1.0.20260528"}
|
soulsoft_web_routing = {version = "1.0.20260528"}
|
||||||
redis = {version = "1.0.20260627"}
|
soulsoft_net_http = {version = "1.0.20260528"}
|
||||||
|
soulsoft_web_hosting = {version = "1.0.20260528"}
|
||||||
soulsoft_web_cors = {version = "1.0.20260528"}
|
soulsoft_web_cors = {version = "1.0.20260528"}
|
||||||
soulsoft_identity_claims = {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_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_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_injection = {version = "1.0.20260528"}
|
||||||
|
soulsoft_extensions_logging = {version = "1.0.20260528"}
|
||||||
|
|||||||
@@ -14,8 +14,10 @@
|
|||||||
package simapi.helpers
|
package simapi.helpers
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
|
import std.io.*
|
||||||
import std.reflect.*
|
import std.reflect.*
|
||||||
import soulsoft_web_http.*
|
import soulsoft_web_http.*
|
||||||
|
import soulsoft_web_mvc.annotations.*
|
||||||
import soulsoft_web_mvc.core.*
|
import soulsoft_web_mvc.core.*
|
||||||
import soulsoft_web_mvc.routing.*
|
import soulsoft_web_mvc.routing.*
|
||||||
import soulsoft_web_mvc.controllers.*
|
import soulsoft_web_mvc.controllers.*
|
||||||
@@ -23,6 +25,7 @@ import soulsoft_web_mvc.modelBindings.*
|
|||||||
import soulsoft_web_mvc.abstractions.*
|
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.attributes.{SimApiAuth as SimApiAuthAttribute, OriginResponse}
|
import simapi.attributes.{SimApiAuth as SimApiAuthAttribute, OriginResponse}
|
||||||
import simapi.communications.*
|
import simapi.communications.*
|
||||||
import simapi.configurations.*
|
import simapi.configurations.*
|
||||||
@@ -60,8 +63,14 @@ struct SimApiActionInvoker {
|
|||||||
public func apply(): Unit {
|
public func apply(): Unit {
|
||||||
let controller = createControllerInstance()
|
let controller = createControllerInstance()
|
||||||
checkSimApiAuth()
|
checkSimApiAuth()
|
||||||
|
// 预读并缓存请求体(body 流不可重读;若请求日志中间件已读,直接用其缓存)
|
||||||
|
if (!context.items.contains(BODY_CACHE_KEY)) {
|
||||||
|
context.items[BODY_CACHE_KEY] = readBody()
|
||||||
|
}
|
||||||
let modelBindingContext = ActionBindingContext(context, actionDescriptor.actionFunction.parameters)
|
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) {
|
if (!modelBindingContext.modelState.isValid) {
|
||||||
handleInvalidModelState(modelBindingContext)
|
handleInvalidModelState(modelBindingContext)
|
||||||
} else {
|
} 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):
|
/// 检查 @SimApiAuth 注解并执行鉴权(对齐 C# SimApiAuthAttribute.OnActionExecuting):
|
||||||
/// 未登录 401 → 类型权限 403 → 遍历执行 ISimApiAuthChecker
|
/// 未登录 401 → 类型权限 403 → 遍历执行 ISimApiAuthChecker
|
||||||
private func checkSimApiAuth() {
|
private func checkSimApiAuth() {
|
||||||
|
|||||||
@@ -122,10 +122,11 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
|
|||||||
read = context.request.body.read(buffer)
|
read = context.request.body.read(buffer)
|
||||||
}
|
}
|
||||||
let bodyText = sb.toString()
|
let bodyText = sb.toString()
|
||||||
// 重置流位置,供后续业务读取
|
// 重置流位置,供后续业务读取;同时缓存 body(流可能不可重读)
|
||||||
if (let seekable: Seekable <- context.request.body) {
|
if (let seekable: Seekable <- context.request.body) {
|
||||||
seekable.seek(SeekPosition.Begin(0))
|
seekable.seek(SeekPosition.Begin(0))
|
||||||
}
|
}
|
||||||
|
context.items["SimApi:BodyCache"] = bodyText
|
||||||
return truncateBody(bodyText)
|
return truncateBody(bodyText)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
return "(读取请求体失败)\n"
|
return "(读取请求体失败)\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user