代码之家  ›  专栏  ›  技术社区  ›  Iain Galloway

ASP.NET MVC中的访问控制取决于输入参数/服务层?

  •  41
  • Iain Galloway  · 技术社区  · 15 年前

    序言:这是一个哲学问题。我更多的是寻找“正确”的方法,而不是“一种”的方法。

    假设我有一些产品,一个ASP.NET MVC应用程序在这些产品上执行CRUD:-

    mysite.example/products/1
    mysite.example/products/1/edit
    

    我使用的是存储库模式,因此这些产品来自何处并不重要:-

    public interface IProductRepository
    {
      IEnumberable<Product> GetProducts();
      ....
    }
    

    任何人都可以查看任何产品,但只有指定为特定产品“管理员”的用户才可以调用编辑操作。

    怎样 我打算在ASP.NETMVC中实现它?除非我遗漏了什么,否则我不能使用内置的ASP.NET授权属性,因为首先我需要为每个产品指定不同的角色,其次,在从存储库检索产品之前,我不知道要检查哪个角色。

    例如,最简单的解决方案如下:-

    public class ProductsController
    {
      public ActionResult Edit(int id)
      {
        Product p = ProductRepository.GetProductById(id);
        User u = UserService.GetUser(); // Gets the currently logged in user
        if (ProductAdminService.UserIsAdminForProduct(u, p))
        {
          return View(p);
        }
        else
        {
          return RedirectToAction("AccessDenied");
        }
      }
    }
    

    • 这些代码中的一些需要重复——想象一下,根据用户与产品的关系,有几个操作(更新、删除、设置库存、订购、CreateOffer)。你必须复制粘贴好几次。
    • 这不是很容易测试的——我每次测试都要计算四个对象。
    • 控制器的“任务”似乎不是检查是否允许用户执行操作。我更希望有一个更具可插拔性(例如,通过属性的AOP)的解决方案。但是,这是否意味着您必须选择两次产品(一次在AuthorizationFilter中,一次在Controller中)?
    • 如果不允许用户发出此请求,返回403是否更好?如果是这样的话,我该怎么做呢?

    当我自己有了新的想法时,我可能会不断更新,但我非常渴望听到你的想法!

    提前谢谢!

    编辑

    编辑2

    开始悬赏这件事。我已经收到了一些好的和有益的答案,但没有什么让我感到舒服的“接受”。请记住,我正在寻找一种干净的方法来保持业务逻辑,以确定索引视图上的“编辑”链接是否显示在确定对产品/Edit/1的请求是否被授权的同一位置。我想把我行动方法中的污染控制在最低限度。理想情况下,我正在寻找基于属性的解决方案,但我承认这可能是不可能的。

    8 回复  |  直到 9 年前
        1
  •  29
  •   Mark Seemann    15 年前

    首先,每个产品都需要一个不同的角色,其次,在从存储库检索到产品之前,我不知道要检查哪个角色

    我见过很多人试图让基于角色的安全性做一些它从未打算做的事情,但您已经超过了这一点,所以这很酷:)

    基于角色的安全性的替代方案是基于ACL的安全性,我认为这就是您在这里需要的。

    您仍然需要检索产品的ACL,然后检查用户是否具有该产品的正确权限。这是如此的上下文敏感和交互繁重,以至于我认为纯粹的声明性方法既过于死板又过于隐式(即,您可能没有意识到在向某些代码添加单个属性时会涉及多少数据库读取)。

    var p = this.ProductRepository.GetProductById(id);
    var user = this.GetUser();
    var permission = new ProductEditPermission(p);
    

    bool canEdit = permission.IsGrantedTo(user);
    

    如果您只想确保用户有权继续,可以发出断言:

    permission.Demand(user);
    

    如果未授予权限,则应引发异常。

    p )具有关联的ACL,如下所示:

    public class Product
    {
        public IEnumerable<ProductAccessRule> AccessRules { get; }
    
        // other members...
    }
    

    您可能想看看System.Security.AccessControl.FileSystemSecurity,以获得有关ACL建模的灵感。

    如果当前用户与Thread.CurrentPrincipal相同(在ASP.NET MVC、IIRC中就是这种情况),则可以将上述权限方法简化为:

    bool canEdit = permission.IsGranted();
    

    permission.Demand();
    

    因为用户是隐式的。您可以查看System.Security.Permissions.PrincipalPermission以获得灵感。

        2
  •  16
  •   David Glenn    15 年前

    从您描述的内容来看,您似乎需要某种形式的用户访问控制,而不是基于角色的权限。如果是这种情况,则需要在整个业务逻辑中实现它。您的场景听起来好像可以在服务层中实现它。

    基本上,您必须从当前用户的角度实现ProductRepository中的所有功能,并且产品被标记为该用户的权限。

    听起来比实际情况更难。首先,您需要一个用户令牌界面,其中包含uid和角色列表的用户信息(如果您想使用角色)。您可以使用IPrincipal,也可以按照

    public interface IUserToken {
      public int Uid { get; }
      public bool IsInRole(string role);
    }
    

    然后在控制器中,将用户令牌解析到存储库构造函数中。

    IProductRepository ProductRepository = new ProductRepository(User);  //using IPrincipal
    

    如果您使用的是FormsAuthentication和自定义IUserToken,那么您可以围绕IPrincipal创建一个包装器,这样您的ProductRepository的创建方式如下:

    IProductRepository ProductRepository = new ProductRepository(new IUserTokenWrapper(User));
    

    现在,所有IPProductRepository函数都应该访问用户令牌以检查权限。例如:

    public Product GetProductById(productId) {
      Product product = InternalGetProductById(UserToken.uid, productId);
      if (product == null) {
        throw new NotAuthorizedException();
      }
      product.CanEdit = (
        UserToken.IsInRole("admin") || //user is administrator
        UserToken.Uid == product.CreatedByID || //user is creator
        HasUserPermissionToEdit(UserToken.Uid, productId)  //other custom permissions
        );
    }
    

    如果您想获得所有产品的列表,可以在数据访问代码中根据权限进行查询。在本例中,使用左连接查看多对多表是否包含UserToken.Uid和productId。如果存在联接的右侧,则您知道用户具有该产品的权限,然后您可以设置product.CanEdit布尔值。

    使用此方法,您可以在视图中使用以下内容(其中模型是您的产品)。

    <% if(Model.CanEdit) { %>
      <a href="/Products/1/Edit">Edit</a>
    <% } %>
    

    或者在你的控制器里

    public ActionResult Get(int id) {
      Product p = ProductRepository.GetProductById(id);
      if (p.CanEdit) {
        return View("EditProduct");
      }
      else {
        return View("Product");
      }
    }
    

    此方法的好处是,安全性内置于服务层(ProductRepository)中,因此它不由控制器处理,也不能被控制器绕过。

    主要的一点是,安全性放在您的业务逻辑中,而不是控制器中。

        3
  •  3
  •   Runeborg    15 年前

    .NET Reflector 查看AuthorizeAttribute是如何实现的,并对其执行您自己的逻辑。

    它所做的是继承FilterAttribute并实现IAuthorizationFilter。我现在不能测试这个,但是类似的东西应该可以用。

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
    public class ProductAuthorizeAttribute : FilterAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }
    
            object productId;
            if (!filterContext.RouteData.Values.TryGetValue("productId", out productId))
            {
                filterContext.Result = new HttpUnauthorizedResult();
                return;
            }
    
            // Fetch product and check for accessrights
    
            if (user.IsAuthorizedFor(productId))
            {
                HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
                cache.SetProxyMaxAge(new TimeSpan(0L));
                cache.AddValidationCallback(new HttpCacheValidateHandler(this.Validate), null);
            }
            else
                filterContext.Result = new HttpUnauthorizedResult();
        }
    
        private void Validate(HttpContext context, object data, ref HttpValidationStatus validationStatus)
        {
            // The original attribute performs some validation in here as well, not sure it is needed though
            validationStatus = HttpValidationStatus.Valid;
        }
    }
    

    您可能还可以将获取的产品/用户存储在filterContext.Controller.TempData中,这样您就可以在控制器中获取它,或者将它存储在某个缓存中。

    编辑:我刚刚注意到关于编辑链接的部分。我能想到的最好的方法是从属性中分解授权部分,并为其生成一个可以在视图中使用的HttpHelper。

        4
  •  1
  •   jimr    15 年前

    我倾向于认为授权是业务逻辑的一部分(或者至少在控制器逻辑之外)。我同意上面kevingessner的观点,授权检查应该是获取物品调用的一部分。在他的OneException方法中,您可以通过以下方式显示登录页面(或在web.config中配置的任何内容):

    if (...)
    {
        Response.StatusCode = 401;
        Response.StatusDescription = "Unauthorized";
        HttpContext.Response.End();
    }
    

    而不是让UserRepository.GetUserSomehowFromTheRequest()调用 对于action方法,我会这样做一次(例如,在Controller.OnAuthorization方法的重写中),然后将该数据粘贴到控制器基类中的某个位置以供以后使用(例如,属性)。

        5
  •  1
  •   tvanfosson    15 年前

    我认为,期望控制器/模型代码控制视图呈现的内容是不现实的,这违反了关注点的分离。控制器/模型代码可以在视图模型中设置一个标志,视图可以使用该标志来确定它应该做什么,但我认为您不应该期望控制器/模型和视图都使用一个方法来控制对模型的访问和渲染。

    话虽如此,您可以用两种方法中的任何一种来实现这一点——这两种方法都涉及到一个视图模型,除了实际模型之外,还包含一些视图使用的注释。在第一种情况下,可以使用属性控制对操作的访问。这是我的首选,但需要独立地装饰每个方法——除非控制器中的所有操作都具有相同的访问属性。

    为此,我开发了一个“角色或所有者”属性。它验证用户是否处于特定角色,或者是否是该方法生成的数据的所有者。在我的例子中,所有权是由用户和相关数据之间的外键关系控制的——也就是说,您有一个ProductOwner表,并且需要有一行包含产品和当前用户的产品/所有者对。它与普通的authorized属性的不同之处在于,当所有权或角色检查失败时,用户将被定向到错误页面,而不是登录页面。在这种情况下,每个方法都需要在视图模型中设置一个标志,指示可以编辑模型。

    或者,您可以在控制器(或基本控制器)的ActionExecuting/ActionExecuted方法中实现类似的代码,以便它在所有控制器中一致地应用。在这种情况下,您需要编写一些代码来检测正在执行的操作类型,以便根据相关产品的所有权确定是否中止该操作。相同的方法将设置标志以指示可以编辑模型。在这种情况下,您可能需要一个模型层次结构,以便可以将模型强制转换为可编辑模型,从而可以设置属性,而不考虑特定的模型类型。

    与使用属性相比,这个选项对我来说似乎更为复杂。对于属性,您可以对其进行设计,使其将各种表和属性名称作为属性的属性,并使用反射根据属性的属性从存储库中获取适当的数据。

        6
  •  0
  •   Iain Galloway    15 年前

    回答我自己的问题(eep!),专业ASP.NET MVC 1.0(NerdDinner教程)的第1章为我推荐了一个类似的解决方案:

    public ActionResult Edit(int id)
    {
      Dinner dinner = dinnerRepositor.GetDinner(id);
      if(!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");
    
      return View(new DinnerFormViewModel(dinner));
    }
    

    为了避免让我饿着肚子吃晚饭,本教程继续在匹配的POST-Action方法和Details视图(实际上是Details视图的子部分)中重复立即实现业务规则的代码,因此本教程并没有添加任何内容

    这是否违反了SRP?如果业务规则发生更改(例如,任何拥有RSVP的人都可以编辑晚餐),则必须同时更改GET和POST方法以及视图(以及删除操作的GET和POST方法和视图,尽管这在技术上是一个单独的业务规则)。

    将逻辑拉入某种权限仲裁器对象(正如我在上面所做的)是否尽可能好?

        7
  •  0
  •   kevingessner    15 年前

    这是正确的,但是可以将所有权限检查封装到一个方法中,如 GetProductForUser ,它接受产品、用户和所需的权限。通过抛出在控制器的OneException处理程序中捕获的异常,处理将全部集中在一个位置:

    enum Permission
    {
      Forbidden = 0,
      Access = 1,
      Admin = 2
    }
    
    public class ProductForbiddenException : Exception
    { }
    
    public class ProductsController
    {
      public Product GetProductForUser(int id, User u, Permission perm)
      {
        Product p = ProductRepository.GetProductById(id);
        if (ProductPermissionService.UserPermission(u, p) < perm)
        {
          throw new ProductForbiddenException();
        }
        return p;
      }
    
      public ActionResult Edit(int id)
      {
        User u = UserRepository.GetUserSomehowFromTheRequest();
        Product p = GetProductForUser(id, u, Permission.Admin);
        return View(p);
      }
    
      public ActionResult View(int id)
      {
        User u = UserRepository.GetUserSomehowFromTheRequest();
        Product p = GetProductForUser(id, u, Permission.Access);
        return View(p);
      }
    
      public override void OnException(ExceptionContext filterContext)
      {
        if (typeof(filterContext.Exception) == typeof(ProductForbiddenException))
        {
          // handle me!
        }
        base.OnException(filterContext);
      }
    }
    

    您只需提供ProductPermissionService.UserPermission,即可返回用户对给定产品的权限。通过使用权限枚举(我想我已经掌握了正确的语法…)并将权限与 <

        8
  •  0
  •   kaptan    13 年前