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

序言:这是一个哲学问题。 我正在寻找更多的“正确”的方式来做到这一点,而不是“一个”的方式来做到这一点。

让我们想象我有一些产品,以及在这些产品上执行CRUD的ASP.NET MVC应用程序:

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

我正在使用存储库模式,所以这些产品来自哪里并不重要:

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

此外,我的存储库还描述了一个用户列表,以及他们pipe理的产品列表(用户和产品之间的很多)。 在应用程序的其他地方,超级pipe理员正在对用户执行CRUD并pipe理用户和他们被允许pipe理的产品之间的关系。

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

应该如何去实现在ASP.NET MVC? 除非我错过了一些东西,否则我不能使用内置的ASP.NET Authorize属性作为第一我需要每个产品的不同angular色,第二我不知道要检查哪个angular色,直到我从存储库检索我的产品。

显然你可以从这个场景概括到大多数内容pipe理场景 – 例如,用户只能编辑他们自己的论坛post。 StackOverflow用户只能编辑自己的问题 – 除非他们有2000或更多的代表…

作为一个例子,最简单的解决办法就是:

 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"); } } } 

我的问题:

  • 其中一些代码需要重复 – 想象有几种操作(更新,删除,SetStock,Order,CreateOffer)取决于用户 – 产品关系。 你必须复制粘贴几次。
  • 这不是非常可测的 – 每次testing你都得模拟四个物体。
  • 看起来好像控制器的“工作”是检查用户是否被允许执行操作。 我宁愿更可插入(例如AOP通过属性)的解决scheme。 但是,这是否意味着您必须两次select产品(一次在AuthorizationFilter中,再次在Controller中)?
  • 如果不允许用户提出这个请求,返回403会更好吗? 如果是的话,我会怎么做呢?

我可能会保持这个更新,因为我自己得到的想法,但我非常渴望听到你的!

提前致谢!

编辑

只是在这里添加一些细节。 我遇到的问题是,我希望业务规则“只有具有权限的用户才可以编辑产品”被包含在一个且只有一个地方。 我觉得确定用户是否可以通过GET或POST进行编辑操作的相同代码也应该负责确定是否在“索引”或“详细信息”视图上显示“编辑”链接。 也许这是不可能/不可行的,但我觉得应该是…

编辑2

从这个开始赏金。 我收到了一些好的和有益的答案,但没有什么让我感觉舒服的“接受”。 请记住,我正在寻找一个很好的干净的方法,以保持业务逻辑,确定是否在编辑索引视图的链接将显示在相同的地方,以确定是否产品/编辑/ 1是否被授权。 我想把我的行动方法中的污染降到最低。 理想情况下,我正在寻找一个基于属性的解决scheme,但我认为这可能是不可能的。

首先,我想你已经半途而废了,因为你说过

首先,我需要每个产品的不同angular色,第二我不知道要检查哪个angular色,直到我从存储库中检索到我的产品

我已经看到了很多尝试使基于angular色的安全性做一些它从来没有打算做的事情,但是你已经过去了,所以这很酷:)

基于angular色的安全性的替代方法是基于ACL的安全性,我认为这就是您需要的。

您仍然需要检索产品的ACL,然后检查用户是否拥有产品的正确权限。 这是如此的上下文敏感和互动的沉重,我认为一个纯粹的声明式的方法既不灵活,也不明确(即你可能不知道有多less数据库读取涉及到添加一个单一的属性到一些代码)。

我认为这样的场景最好是由一个封装了ACL逻辑的类来模拟,这样您就可以根据当前的上下文来查询决策或进行断言 – 如下所示:

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

如果您只想知道用户是否可以编辑产品,则可以发出查询:

 bool canEdit = permission.IsGrantedTo(user); 

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

 permission.Demand(user); 

如果权限未被授予,这应该会引发exception。

这一切都假定Product类(variablesp )具有关联的ACL,如下所示:

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

您可能需要查看System.Security.AccessControl.FileSystemSecurity以获取有关对ACL进行build模的灵感。

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

 bool canEdit = permission.IsGranted(); 

要么

 permission.Demand(); 

因为用户是隐含的。 你可以看看System.Security.Permissions.PrincipalPermission的灵感。

从你所描述的这听起来像你需要某种forms的用户访问控制,而不是基于angular色的权限。 如果是这种情况,则需要在整个业务逻辑中实施。 你的scheme听起来像你可以在你的服务层实现它。

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

这听起来比实际上更困难。 首先你需要一个包含uid和angular色列表(如果你想使用angular色)的用户信息的用户标记接口。 您可以使用IPrincipal或创build自己的行

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

然后在您的控制器中,将用户令牌parsing为您的Repository构造函数。

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

如果你使用FormsAuthentication和一个自定义的IUserToken,那么你可以在IPrincipal上创build一个Wrapper,这样你的ProductRepository就可以像下面这样创build了:

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

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

 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布尔值。

使用这种方法,您可以在视图中使用以下(如果您喜欢)(其中Model是您的产品)。

 <% 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)中,所以它不被控制器处理,不能被控制器绕过。

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

复制粘贴解决scheme真的变得乏味一段时间,真的很烦人维护。 我可能会去做一个你需要的自定义属性。 您可以使用优秀的.NET Reflector来查看AuthorizeAttribute是如何实现的,并执行您自己的逻辑。

它所做的是inheritanceFilterAttribute并实现IAuthorizationFilter。 我目前无法testing,但是这样的事情应该可以工作。

 [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中,以便您可以在控制器中获取它,或将其存储在某个caching中。

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

我倾向于认为授权是您业务逻辑的一部分(或者至less在您的控制器逻辑之外)。 我同意上面的kevingessner,因为授权检查应该是获取该项目的呼叫的一部分。 在他的OnException方法中,可以通过如下方式显示login页面(或者你在web.config中configuration的任何东西):

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

而不是在所有的操作方法中进行UserRepository.GetUserSomehowFromTheRequest()调用,我会这样做一次(例如覆盖Controller.OnAuthorization方法),然后将这些数据粘贴到控制器基类的某处以备后用(例如一个财产)。

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

话虽如此,你可以通过两种方式来解决这个问题 – 两种方式都会涉及一个视图模型,除了实际的模型之外,视图模型还包含视图使用的一些注释。 在第一种情况下,您可以使用属性来控制对操作的访问。 这将是我的偏好,但会涉及独立装饰每个方法 – 除非控制器中的所有操作具有相同的访问属性。

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

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

这个选项似乎比使用属性更复杂,可以说是更复杂。 在属性的情况下,您可以devise它,以便它将各种表和属性名称作为属性的属性,并使用reflection根据属性的属性从存储库中获取正确的数据。

回答我自己的问题(eep!),专业ASP.NET MVC 1.0(NerdDinner教程)的第1章推荐了类似的解决scheme来解决上面的问题:

 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动作方法中立即重复执行业务规则的代码,并在详细信息视图中(实际上在子视图的子部分详细视图)

这是否违反SRP? 如果业务规则发生了变化(例如,任何有RSVP的人都可以编辑晚餐),那么你必须改变GET和POST方法,同时也要改变View(以及GET和POST方法和View的删除操作,尽pipe这在技术上是一个独立的业务规则)。

是否将逻辑拉入某种权限仲裁对象(就像我上面所做的)一样好?

您处于正确的轨道上,但是您可以将所有权限检查封装到一个方法中,例如GetProductForUser ,该方法需要产品,用户和所需的权限。 通过抛出在控制器的OnException处理程序中捕获的exception,处理全部在一个地方:

 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,即可对给定的产品返回用户的权限。通过使用Permission枚举(我认为我的语法是正确的…),并将权限与< ,Admin权限隐含访问权限进行比较,这几乎总是正确的。

您可以使用基于XACML的实现。 通过这种方式,您可以外部化授权,还可以在代码之外为您的策略创build存储库。