我有一个基于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可能会尝试提交值,尽管他/她实际上没有看到相应的表单控件,因为他/她没有声明。
如果用户没有这些声明,无论他为这些字段发送什么值,我都希望设置用于字段值的默认值。
有内置的东西吗?
[布莱恩·刘易斯(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.");
}
});
}
}
您可以验证该值,但不应设置该值。
有内置的东西吗?
没有没有内置的方法可以做到这一点。
您可能希望使用自定义模型活页夹来实现。但是我认为这不是一个好方法。因为您必须同时处理各种输入格式化程序。考虑一下您的动作期望[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);
}
我创建了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
您实际上要执行两个操作-验证和对传入模型进行条件编辑。对于复杂的验证,您应该考虑使用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/