ASP.NET Core根据声明提交值

问题描述 投票:0回答:3

我有一个基于MVC的HTML表单,该表单已通过POST发送到控制器操作。

表格包含不同的输入。如果用户有某些声明,则还有其他输入。例如,如果用户是管理员,则他/她会看到一个附加的文本区域以供注释。

public class MySubmit
{
    public string Name { get; set; }
    public string IsActive { get; set; }

    // only an administrator should be able to set this field
    // for all other users, this should be empty
    public string Comment { get; set; }
}

public class MyController : Controller
{
    public IActionResult MyActionResult(MySubmit submit)
    {

    }
}

处理动作结果的最佳和最安全的方法是什么?从理论上讲,a可能会尝试提交值,尽管他/她实际上没有看到相应的表单控件,因为他/她没有声明。

如果用户没有这些声明,无论他为这些字段发送什么值,我都希望设置用于字段值的默认值。

有内置的东西吗?

c# asp.net asp.net-core
3个回答
1
投票

[布莱恩·刘易斯(Bryan Lewis)给出了正确的提示:流利的验证。

Fluent Validation能够通过依赖注入使用HTTP上下文来接收用户并执行声明比较:

public class YourModelValidator: AbstractValidator<YourModel>
{
    public YourModelValidator(IHttpContextAccessor httpContext)
    {
        RuleFor(x => x.YourProprty).Custom( (html, context) =>
        {
            var user = httpContext.User;

            if (!user.HasClaim(c => c.Type.Equals(claim))
            {
                context.AddFailure("Claim is missing.");
            }
        });
    }
}

您可以验证该值,但不应设置该值。


2
投票

有内置的东西吗?

没有没有内置的方法可以做到这一点。

设计

您可能希望使用自定义模型活页夹来实现。但是我认为这不是一个好方法。因为您必须同时处理各种输入格式化程序。考虑一下您的动作期望[FromForm]MySubmit mySubmit的某个地方,而另一个动作期望[FromBody] Submit mySubmit的某个地方。第一个动作需要形式的有效负载,而第二个动作可能需要JSON。即使您照顾了以上两种情况,将来要启用XML负载又如何呢?简而言之,您几乎无法为此编写通用的Model Binder。

验证可能会有所帮助。但是,如果有多个模型,验证通常会使您重复自己(考虑一下您有十个领域模型,每个模型都有几个属性,这些属性需要一些声明)

IMO,更好的方法是使用ActionFilter。由于ActionFilter在模型绑定之后发生,因此当字段需要角色时,可以删除该字段。

为此,创建一个自定义属性以标记哪个属性需要某些角色:

[AttributeUsage(AttributeTargets.Property, AllowMultiple=false)]
internal class RequireRolesForBindingAttribute : Attribute
{
    public string[] Roles {get;}
    public RequireRolesForBindingAttribute(params string[] roles)
    {
        this.Roles = roles;
    }
}

现在,当需要某些角色时,只需如下所示注释目标属性:

public class MySubmit
{
    public string Name { get; set; }
    public string IsActive { get; set; }

    // only an root/admin can bind this field for all other users, this should be empty
    [RequireRolesForBindingAttribute("root","admin")]
    public string Comment { get; set; }

    public Sub Sub{get;set;}    // test it with a complex child 
}

public class Sub{
    public int Id {get;set;}
    public string Name {get;set;}

    [RequireRolesForBindingAttribute("root","admin")]
    public string Note {get;set;}
}

以上数据注释表示如果用户没有权限,则应删除这两个属性:

  • Comment的[MySubmit属性
  • Note的[Sub属性

最后,不要忘记启用自定义操作过滤器。例如,将其添加到操作方法上:

[TypeFilter(typeof(RequireRolesForBindingFilter))]
public IActionResult Test(MySubmit mySubmit)
{
    return Ok(mySubmit);
}

[C0的实现

我创建了RequireRolesForBindingFilter的实现供您参考:

RequireRolesForBindingFilter

演示:

[当某位无权提交评论和注释的用户发送有效载荷时,如下所示:

public class RequireRolesForBindingFilter : IAsyncActionFilter
{
    private readonly IAuthorizationService _authSvc;

    public RequireRolesForBindingFilter(IAuthorizationService authSvc)
    {
        this._authSvc = authSvc;
    }
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // skip early when User ==null, 
        //    if you don't want to allow anonymous access, use `[Authorize]`
        if(context.HttpContext.User !=null) {  
            await this._checkUserRights(context.ActionArguments, context.HttpContext.User);
        }
        await next();
    }
    private async Task _checkUserRights(IDictionary<string, object> args, ClaimsPrincipal user){
        // handle each argument
        foreach(var kvp in args){
            if(kvp.Value==null) { return; }
            var valueType = kvp.Value.GetType();
            if(await _shouldSetNullForType(valueType, user)) {
                args[kvp.Key] = valueType.IsValueType? Activator.CreateInstance(valueType) : null;
            }else{
                // handle each property of this argument
                foreach(var pi in valueType.GetProperties())
                {
                    var pv = pi.GetValue(kvp.Value);
                    await _checkPropertiesRecursive( instanceValue: kvp.Value, propInfo: pi, user: user);
                }
            }
        }

        async Task<bool> _shouldSetNullForType(Type type, ClaimsPrincipal user)
        {
            // the `RequireRolesForBindingAttribute`
            var attr= type 
                .GetCustomAttributes(typeof(RequireRolesForBindingAttribute), false)
                .OfType<RequireRolesForBindingAttribute>()
                .FirstOrDefault();
            return await _shouldSetNullForAttribute(attr,user);
        }
        async Task<bool> _shouldSetNullForPropInfo(PropertyInfo pi, ClaimsPrincipal user)
        {
            // the `RequireRolesForBindingAttribute`
            var attr= pi
                .GetCustomAttributes(typeof(RequireRolesForBindingAttribute), false)
                .OfType<RequireRolesForBindingAttribute>()
                .FirstOrDefault();
            return await _shouldSetNullForAttribute(attr,user);
        }
        async Task<bool> _shouldSetNullForAttribute(RequireRolesForBindingAttribute attr, ClaimsPrincipal user)
        {
            if(attr!=null) {
                var policy = new AuthorizationPolicyBuilder().RequireRole(attr.Roles).Build();
                // does the user have the rights?
                var authResult = await this._authSvc.AuthorizeAsync(user, null, policy);
                if(!authResult.Succeeded){ 
                    return true;
                }
            }
            return false;
        }
        // check one property (propInfo) for instance `instanceValue`
        async Task _checkPropertiesRecursive(object instanceValue, PropertyInfo propInfo,  ClaimsPrincipal user){
            if(instanceValue == null) return;
            Type propType = propInfo.PropertyType;
            object propValue = propInfo.GetValue(instanceValue);
            if(await _shouldSetNullForPropInfo(propInfo, user))
            {
                propInfo.SetValue(instanceValue, propType.IsValueType? Activator.CreateInstance(propType) : null);
            }
            else if( !shouldSkipCheckChildren(propType) && propValue!=null ){ 
                // process every sub property for this propType
                foreach(var spi in propType.GetProperties()) 
                {
                    await _checkPropertiesRecursive(instanceValue: propValue , spi, user );
                }
            }

            bool shouldSkipCheckChildren(Type type) => (type == typeof(string) || type == typeof(DateTime));
        }
    }
}

响应将是:

POST https://localhost:5001/home/test
cookie: <my-cookie>
Content-Type: application/x-www-form-urlencoded

name=a&isActive=true&comment=abc&sub.Name=s1&sub.note=magic

0
投票

您实际上要执行两个操作-验证和对传入模型进行条件编辑。对于复杂的验证,您应该考虑使用FluentValidation(HTTP/1.1 200 OK Connection: close Content-Type: application/json; charset=utf-8 Server: Kestrel Transfer-Encoding: chunked { "name": "a", "isActive": "true", "comment": null, "sub": { "id": 0, "name": "s1", "note": null } } )之类的工具,该工具非常灵活,与MVC的ModelState集成,将允许您根据条件检查各种事物。从您的帖子中尚不清楚是在一般意义上指的是“声明”,还是专门指ASP.Net身份声明-无论哪种方式,您都可以将身份信息放入FluentValidation Validator并基于身份信息创建条件检查。验证器(FV或其他方式)实际上无法处理重置/编辑模型。以您的示例为例,您可以在验证完成后直接更改模型。

https://fluentvalidation.net/
© www.soinside.com 2019 - 2024. All rights reserved.