You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Asp.Net中如何基于BindingSource手动实现请求模型绑定?

在IAuthorizationHandler中手动解析Asp.Net请求数据的实现方案

核心思路

直接复用Asp.Net内置的模型绑定机制,避免重复实现类型转换、格式校验等逻辑,同时结合你已定义的ITargetEntityContract和TargetEntityLocation属性,完成请求数据解析与权限校验的联动。

步骤实现

1. 完善TargetEntityLocation属性定义

确保属性能存储目标模型类型(需实现ITargetEntityContract)和对应的绑定源:

[AttributeUsage(AttributeTargets.Method)]
public class TargetEntityLocationAttribute : Attribute
{
    public Type ModelType { get; }
    public BindingSource BindingSource { get; }

    public TargetEntityLocationAttribute(Type modelType, BindingSource bindingSource)
    {
        ModelType = modelType;
        BindingSource = bindingSource;
    }
}

// 示例ITargetEntityContract接口
public interface ITargetEntityContract
{
    int GetTargetEntityId();
}

2. 实现自定义AuthorizationHandler

通过注入Asp.Net模型绑定核心组件,完成请求数据的解析与权限校验:

public class TargetEntityAuthorizationHandler : AuthorizationHandler<TargetEntityRequirement>
{
    private readonly IModelBinderFactory _modelBinderFactory;
    private readonly ICompositeModelBinderProvider _modelBinderProvider;

    public TargetEntityAuthorizationHandler(IModelBinderFactory modelBinderFactory, ICompositeModelBinderProvider modelBinderProvider)
    {
        _modelBinderFactory = modelBinderFactory;
        _modelBinderProvider = modelBinderProvider;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, TargetEntityRequirement requirement)
    {
        // 获取当前端点元数据
        if (context.Resource is not Endpoint endpoint)
        {
            context.Fail();
            return;
        }

        var locationAttr = endpoint.Metadata.OfType<TargetEntityLocationAttribute>().FirstOrDefault();
        if (locationAttr == null || !typeof(ITargetEntityContract).IsAssignableFrom(locationAttr.ModelType))
        {
            context.Fail();
            return;
        }

        // 构建模型绑定上下文
        var modelBindingContext = CreateModelBindingContext(context.HttpContext, locationAttr);
        if (modelBindingContext == null)
        {
            context.Fail();
            return;
        }

        // 执行模型绑定
        var binder = _modelBinderFactory.CreateBinder(modelBindingContext);
        await binder.BindModelAsync(modelBindingContext);

        if (modelBindingContext.Result.IsModelSet && modelBindingContext.Result.Model is ITargetEntityContract entityContract)
        {
            var targetEntityId = entityContract.GetTargetEntityId();
            // 结合权限要求完成校验逻辑
            if (ValidateUserAccess(context.User, requirement, targetEntityId))
            {
                context.Succeed(requirement);
                return;
            }
        }

        context.Fail();
    }

    private ModelBindingContext? CreateModelBindingContext(HttpContext httpContext, TargetEntityLocationAttribute locationAttr)
    {
        var metadataProvider = httpContext.RequestServices.GetRequiredService<IModelMetadataProvider>();
        var modelMetadata = metadataProvider.GetMetadataForType(locationAttr.ModelType);

        return new DefaultModelBindingContext
        {
            ActionContext = new ActionContext(httpContext, httpContext.GetRouteData(), new ControllerActionDescriptor()),
            ModelMetadata = modelMetadata,
            ModelName = string.Empty,
            ValueProvider = GetValueProvider(httpContext, locationAttr.BindingSource, locationAttr.ModelType)
        };
    }

    private IValueProvider? GetValueProvider(HttpContext httpContext, BindingSource bindingSource, Type modelType)
    {
        if (bindingSource == BindingSource.Path) // 对应FromRoute
        {
            return new RouteValueProvider(BindingSource.Path, httpContext.GetRouteData().Values);
        }
        else if (bindingSource == BindingSource.Query) // 对应FromQuery
        {
            return new QueryStringValueProvider(BindingSource.Query, httpContext.Request.Query, CultureInfo.CurrentCulture);
        }
        else if (bindingSource == BindingSource.Body) // 对应FromBody
        {
            // 启用请求体重读,避免流读取一次后无法访问
            httpContext.Request.EnableBuffering();
            using var reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, leaveOpen: true);
            var bodyContent = await reader.ReadToEndAsync();
            httpContext.Request.Body.Position = 0;

            // 解析请求体为目标模型,再构造ValueProvider
            var model = JsonSerializer.Deserialize(bodyContent, modelType);
            return new ObjectValueProvider(new EmptyModelMetadataProvider().GetMetadataForType(modelType), model);
        }
        return null;
    }

    private bool ValidateUserAccess(ClaimsPrincipal user, TargetEntityRequirement requirement, int targetEntityId)
    {
        // 替换为你的实际权限校验逻辑,比如检查用户是否拥有该实体的操作权限
        return user.HasClaim(c => c.Type == "EntityPermission" && c.Value == $"{targetEntityId}:{requirement.Permission}");
    }
}

// 示例权限要求
public class TargetEntityRequirement : IAuthorizationRequirement
{
    public string Permission { get; set; } = "Read";
}

3. 注册服务与应用属性

在Program.cs中注册授权服务:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("TargetEntityAccess", policy =>
        policy.Requirements.Add(new TargetEntityRequirement { Permission = "Read" }));
});
builder.Services.AddSingleton<IAuthorizationHandler, TargetEntityAuthorizationHandler>();

在控制器端点上应用属性:

[HttpGet("/api/entities/{id}")]
[TargetEntityLocation(typeof(EntityQueryRequest), BindingSource.Path)]
[Authorize(Policy = "TargetEntityAccess")]
public IActionResult GetEntity(int id)
{
    // 业务逻辑
    return Ok();
}

// 示例请求模型
public class EntityQueryRequest : ITargetEntityContract
{
    public int Id { get; set; }
    public int GetTargetEntityId() => Id;
}

关键注意点

  • 请求体重读:处理FromBody时必须调用EnableBuffering(),并重置流位置,否则后续中间件或控制器无法读取请求体。
  • 类型安全校验:获取TargetEntityLocationAttribute后,必须校验ModelType是否实现ITargetEntityContract,避免类型转换错误。
  • 模型绑定上下文:确保ActionContext、ModelMetadata和ValueProvider配置正确,否则模型绑定会失败。

内容的提问来源于stack exchange,提问作者Marcel Lorenz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 02:01:10