【问题标题】:ASP.NET Core 3.1 Web Api authorization on model property level模型属性级别的 ASP.NET Core 3.1 Web Api 授权
【发布时间】:2020-09-27 12:30:13
【问题描述】:

我有一个带有基本 jwt 身份验证和基于角色授权的 web api。现在我想限制某些字段被角色 user 中的用户编辑,因为基于路由的授权是不够的。

class Account {
    public int Id {get; set;}
    public string Email {get; set;}
    public string Password {get; set;}
    public bool Enabled {get; set;} // <- this field should only be editable by an admin or manager
    public int RoleId {get; set;} // <- this field should only be editable by an admin
}

当用户是角色用户时,他只能更改他的电子邮件地址和密码,但仅限于他的帐户。当他在角色管理器中时,他应该能够编辑字段电子邮件、密码和启用,但仅限于用户角色中的帐户。管理员可以编辑每个用户的每个字段。

有什么可以解决我的问题的,例如这样的:

class Account {
    public int Id {get; set;}
    public string Email {get; set;}
    public string Password {get; set;}

    [Authorize(Roles = "Admin,Manager")]
    public bool Enabled {get; set;} // <- this field should only be editable by an admin or manager

    [Authorize(Roles = "Admin")]
    public int RoleId {get; set;} // <- this field should only be editable by an admin
}

关于我的项目的更多信息: - ASP.NET 核心 3.1 - 我使用 Entity Framework Core 和 Postgres 数据库 - 对于身份验证,我使用基本的 jwt 不记名身份验证

【问题讨论】:

  • 据我所知,目前还没有内置功能可以实现这个需求,你必须在你的业务逻辑中判断当前用户,你可以跟踪哪个字段被修改,然后再决定是否按照当前用户角色继续操作。另外,我在github上创建了VOC,你可以参考这个link来检查PG是否开启了这个功能。
  • 这个问题可以在 Node.js 中使用 Plumier request/response body authorization轻松解决
  • 框架中没有类似的内置功能~现在~,但是,只需创建两个具有基于路由授权的端点,一个能够修改此字段,另一个没有,不`不解决你的问题?或者在一个路由上分离两个方法,然后检查用户是否是管理员,走一条路,如果不是,走这条路
  • 一个好的解决方法是将您想要的“只读”值从联系人类中提取到具有私有设置器的另一个 ContactAdminClass 中,并让您的常规联系人类继承自该类。这应该可以最大限度地减少您必须做的工作量,并确保您的普通用户无法编辑 Id。您需要小心地将类正确地转换为 ContactAdmin 以便从您的管理员端进行编辑。
  • 您可以像这样制作自己的属性,并在设置逻辑中使用反射。

标签: c# asp.net asp.net-core .net-core entity-framework-core


【解决方案1】:

所以,我认为您对 Authtorize 工作的理解不正确。

此属性用于控制器。您可以创建多个控制器并为每个方法设置不同的 ROLES 以指定哪些角色可以调用此方法。

在 Dto(数据传输对象)类上指定它是不正确的。

但是您可以使用 2 个控制器和继承来制作一些有趣的解决方案。

//Account dto for edit
class AccountEditDto {
    public int Id {get; set;}
    public string Email {get; set;}
    public string Password {get; set;}
}

//Controller to edit account
[Route("all/account_controller")]
public class AccountController : Controller
{
    
    public ActionResult EditAccount(AccountEditDto accountDto)
    {
        //do something
    }
}

然后创建经理角色设置如下:

//Account dto for edit
class AccountManagerEditDto : AccountEditDto {
    public bool Enabled {get; set;} 
}

//Controller admin to edit account
[Area("Manager")]
[Route("manager/account_controller")]
public class AccountManagerController : AccountController
{
    [Authorize(Roles = "Manager")]
    public ActionResult EditAccount(AccountManagerEditDto accountDto)
    {
        //Do something
    }
}

然后创建管理员角色设置如下:

//Account dto for edit
class AccountAdminEditDto : AccountManagerEditDto {
    public int RoleId {get; set;} 
}

//Controller admin to edit account
[Area("Admin")]
[Route("admin/account_controller")]
public class AccountAdminController : AccountController
{
    [Authorize(Roles = "Admin")]
    public ActionResult EditAccount(AccountAdminEditDto accountDtp)
    {
        //Do something
    }
}

然后你可以使用比 URL 模式调用控制器方法:

http://localhost/{role}/accont_controller/edit

【讨论】:

    【解决方案2】:

    jwt 是基于令牌的,这意味着每个用户都有自己的唯一令牌,用于访问您的系统。

    实现此目的的最简单方法是解码令牌并检查角色,从而使您能够为用户设置角色特定的操作。

    您将无法通过属性执行此操作,因为jwt 不支持它们。 有一个很好的指南可以引导您完成它,因为这里的答案太长了:

    Role based JWT tokens

    它需要对令牌的工作原理有基本的了解,并且该指南提供了简要说明。

    【讨论】:

      【解决方案3】:

      虽然我不是这个答案的真正粉丝,但你可能需要这个答案,因为你的工作场所的内部政策。

      免责声明:: 这个问题被视为业务规则验证,因为从技术上讲,有很多方法可以将它们分开。

      对我来说,我喜欢第一个答案,它很好地隔离了 API....

      虽然这是您正在寻找的想法,但您必须将它们保存在一个 api 中::

          [System.AttributeUsage(System.AttributeTargets.Property, Inherited = true)]
      public class AuthorizeEdit : ValidationAttribute
      {
          private readonly string _role;
          private IHttpContextAccessor _httpContextAccessor;
          public AuthorizeEdit(string role) : base()
          {
              _role = role;
          }
          public override bool IsValid(object value)
          {
              return _httpContextAccessor.HttpContext.User?.IsInRole(_role) ?? false;
          }
          protected override ValidationResult IsValid(object value, ValidationContext validationContext)
          {
      
              _httpContextAccessor = (IHttpContextAccessor)validationContext
                         .GetService(typeof(IHttpContextAccessor));
      
              return base.IsValid(value, validationContext);
      
          }
      
      }
      

      你可以这样使用它:

      class Account {
      public int Id {get; set;}
      public string Email {get; set;}
      public string Password {get; set;}
      
      
      public bool Enabled {get; set;} // <- this field should only be editable by an admin or manager
      
      [Authorize(Roles = "Admin")]
      public int RoleId {get; set;} // <- this field should only be editable by an admin
      

      }

      缺点:任何不在验证者角色中的人都将无法更改任何失败的东西

      我的建议:像第一个答案建议那样分开您的 API,然后将此过滤器添加到管理 API 中

      请注意,您可以将过滤器更改为您想要的角色

      【讨论】:

        【解决方案4】:

        很遗憾,这是不可能,因为它不是 ASP.NET Core 3.1 的内置功能。

        但是,为什么不在处理程序中执行您的逻辑呢?

        您可以为用户创建多个端点(我不推荐),或者按照惯例,使用 1 条路由并在处理数据之前根据用户的角色验证数据。

        获取当前帐户,检查发生了什么变化,如果用户更改了他们无权更改的属性,则返回 HTTP 403 Forbidden 而不进一步处理他们的请求。

        如果他们有合适的角色来执行该操作,请照常继续。

        【讨论】:

          【解决方案5】:

          您可以继承AuthorizeAttribute 并像这样编写自己的:

          public class myAuthorizationAttribute : AuthorizeAttribute
          {
              protected override bool IsAuthorized(HttpActionContext actionContext)
              {
                  // do any stuff here
                  // it will be invoked when the decorated method is called
                  if (CheckAuthorization(actionContext)) 
                     return true; // authorized
                  else
                     return false; // not authorized
              }
          
              private bool CheckAuthorization(HttpActionContext actionContext)
              {
                  bool isAuthorized = ... // determine here if user is authorized
                  if (!isAuthorized) return false;
          
                  var controller = (myController)actionContext.ControllerContext;
          
                  // add those boolean properties to myController
                  controller.isEnabledReadOnly = ... // determine here if user has the role
                  controller.isRoleIdReadOnly  = ... // determine here if user has the role
                  return true;
              }
          
          }
          

          CheckAuthorization 函数中,您可以检查可用角色,然后在代码中设置标志以决定是否应允许写入相关字段。

          它可以简单地用于任何方法,例如:

          [myAuthorization]
          public HttpResponseMessage Post(string id)
          {
              // ... your code goes here
              response = new HttpResponseMessage(HttpStatusCode.OK); // return OK status
              return response;
          }
          

          由于您已将属性isEnabledReadOnlyisRoleIdReadOnly 添加到您的控制器中,您可以通过任何(Get、Post、...)方法直接访问它们。

          注意:您可以使用actionContext 来访问请求、响应、ControllerContext 等(参见here)。

          有关内部结构(如何工作)的更多信息,请访问here.

          【讨论】:

            【解决方案6】:

            你可以在一个动作中做。你应该编写这样的代码。

            public class AccountController : Controller
            {
                [HttpPost("EditProfile")]
                public async Task<IActionResult> EditProfile(User user)
                {
                    var fields = new List<string>();
                    fields.Add("Id");
                    fields.Add("Email");
                    fields.Add("Password");
            
                    if (User.IsInRole("Admin")){
                       fields.Add("RoleId");
                       fields.Add("Enabled ");
                    }else if(User.IsInRole("Manager"))){
                       fields.Add("Enabled ");
                    }
            
                     var updateUser = context.Entry(user);
                        
                        foreach (var field in fields)
                        {
                            updateUser.Property(field).IsModified = true;
                        }
            
                        await context.SaveChangesAsync();
                    
                }
            
                
            }
            

            【讨论】:

              【解决方案7】:

              好吧,我个人建议使用@TemaTre 的答案,但既然你问了。除了使用自定义序列化程序之外,我还可以提及另一种可能性。即使用自定义模型绑定器。

              您需要几个基本步骤

              1. 定义自定义属性
              [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
              public sealed class AuthorizePropertyAttribute : Attribute
              {
                readonly string roles;
              
                 public AuthorizePropertyAttribute(string roles)
                 {
                    this.roles = roles;
                 }
              
                 public string Roles
                 {
                     get { return roles; }
                 }
              }
              
              1. 使用该属性注释您的模型
              public class Account
              {
                  public int Id { get; set; }
                  public string Email { get; set; }
                  public string Password { get; set; }
              
                  [AuthorizeProperty("Admin,Manager")]
                  public bool Enabled { get; set; } // <- this field should only be editable by an admin or manager
              
                  [AuthorizeProperty("Admin")]
                  public int RoleId { get; set; } // <- this field should only be editable by an admin
              }
              
              1. 定义自定义ModelBinder
              public class AuthorizedModelBinder : IModelBinder
              {
                 public async Task BindModelAsync(ModelBindingContext bindingContext)
                 {
                    using var reader = new StreamReader(bindingContext.HttpContext.Request.Body); 
                    var body = await reader.ReadToEndAsync();
                    var jObject = JsonConvert.DeserializeObject(body, bindingContext.ModelType); // get the posted object
                    var modelType = bindingContext.ModelType;
                    var newObject = Activator.CreateInstance(modelType); // this is for demo purpose you can get it in a way you want (like reading it from db)
                    var properties = modelType.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
                    var user = bindingContext.HttpContext.User; // this is also for demo purpose only
              
                     foreach (var prop in properties)
                     {
                        var auth = prop
                                  .GetCustomAttributes(typeof(AuthorizePropertyAttribute), true)
                                  .OfType<AuthorizePropertyAttribute>().FirstOrDefault(); // check the property has that auth attribute
                         if (auth == null)
                         {
                             prop.SetValue(newObject, prop.GetValue(jObject)); // if not assign that property
                         }
                         else
                         {
                             var isInRole = auth.Roles.Split(",", StringSplitOptions.RemoveEmptyEntries).Any(user.IsInRole);
                             if (isInRole) // this guy has access
                             {
                                 prop.SetValue(newObject, prop.GetValue(jObject));
                             }
                          }
                      }
                  }
              }
              

              最后,如果您希望每个 Account 对象都通过此模型绑定器,您可以像这样注释您的类:

              [ModelBinder(typeof(AuthorizedModelBinder))]
              public class Account ...
              

              或者你可以在你想使用的Action上指定它:

              public IActionResult Sup([ModelBinder(typeof(AuthorizedModelBinder))]Account ACC)...
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2020-10-27
                • 2023-03-12
                • 2020-04-11
                • 2021-03-12
                • 2020-05-19
                • 2020-03-06
                • 2020-05-18
                • 2018-08-19
                相关资源
                最近更新 更多