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

Iai*_*way 41 asp.net-mvc

序言:这是一个哲学问题.我正在寻找更好的"正确"方式来做到这一点而不是"一种"方式来做到这一点.

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

mysite.example/products/1
mysite.example/products/1/edit
Run Code Online (Sandbox Code Playgroud)

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

public interface IProductRepository
{
  IEnumberable<Product> GetProducts();
  ....
}
Run Code Online (Sandbox Code Playgroud)

我的存储库还描述了一个用户列表,以及他们管理的产品(用户和产品之间的许多产品).在应用程序的其他地方,Super-Admin正在对用户执行CRUD并管理用户与他们被允许管理的产品之间的关系.

任何人都可以查看任何产品,但只允许为特定产品指定为"管理员"的用户调用例如编辑操作.

应该如何在ASP.NET MVC中实现它?除非我错过了什么,否则我不能使用内置的ASP.NET Authorize属性,因为我需要为每个产品使用不同的角色,其次我不知道要检查哪个角色直到我从存储库中检索了我的产品.

显然,您可以将此场景概括为大多数内容管理方案 - 例如,用户只能编辑自己的论坛帖子.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");
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

我的问题:

  • 这些代码中的一部分需要重复 - 假设有几个操作(Update,Delete,SetStock,Order,CreateOffer),这取决于User-Products关系.你必须多次复制粘贴.
  • 它不是很可测试 - 你必须通过我的计数来模拟每个测试的四个对象.
  • 检查用户是否被允许执行操作似乎并不像控制器的"工作".我更倾向于更可插拔(例如通过属性的AOP)解决方案.但是,这是否意味着您必须两次选择产品(一次在AuthorizationFilter中,一次在Controller中)?
  • 如果不允许用户提出此请求,返回403会更好吗?如果是这样,我将如何做到这一点?

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

提前致谢!

编辑

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

编辑2

在这个开始赏金.我收到了一些好的和有用的答案,但没有什么我觉得"接受".请记住,我正在寻找一个很好的干净方法来保持业务逻辑,以确定索引视图上的"编辑"链接是否将显示在同一位置,以确定是否对产品/编辑请求/ 1是否被授权.我想把我的行动方法中的污染保持在最低限度.理想情况下,我正在寻找基于属性的解决方案,但我接受这可能是不可能的.

Mar*_*ann 29

首先,我认为你已经中途考虑过了,因为你说过

首先,我需要为每个产品使用不同的角色,其次我不知道要检查哪个角色,直到我从存储库中检索到我的产品

我已经看到很多尝试使基于角色的安全做一些从未打算做的事情,但是你已经超越了那一点,所以这很酷:)

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

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

我觉得像这样的情况下,最好由封装ACL逻辑的类来模拟,让您无论是查询的或者作出断言基于当前环境 - 是这样的:

var p = this.ProductRepository.GetProductById(id);
var user = this.GetUser();
var permission = new ProductEditPermission(p);
Run Code Online (Sandbox Code Playgroud)

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

bool canEdit = permission.IsGrantedTo(user);
Run Code Online (Sandbox Code Playgroud)

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

permission.Demand(user);
Run Code Online (Sandbox Code Playgroud)

如果未授予权限,则应该抛出异常.

这都假定Product类(变量p)具有关联的ACL,如下所示:

public class Product
{
    public IEnumerable<ProductAccessRule> AccessRules { get; }

    // other members...
}
Run Code Online (Sandbox Code Playgroud)

您可能需要查看System.Security.AccessControl.FileSystemSecurity以获取有关建模ACL的灵感.

如果当前用户相同Thread.CurrentPrincipal中(这是在ASP.NET MVC,IIRC的情况下),你可以simplyfy上述权限的方法:

bool canEdit = permission.IsGranted();
Run Code Online (Sandbox Code Playgroud)

要么

permission.Demand();
Run Code Online (Sandbox Code Playgroud)

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

  • 您是否拥有或知道在MVC应用程序中使用基于ACL的安全性的示例? (26认同)

Dav*_*enn 16

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

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

这听起来比实际更困难.首先,您需要一个用户令牌接口,其中包含uid和角色列表的用户信息(如果您想使用角色).您可以使用IPrincipal或创建自己的行

public interface IUserToken {
  public int Uid { get; }
  public bool IsInRole(string role);
}
Run Code Online (Sandbox Code Playgroud)

然后在您的控制器中,您将用户令牌解析到您的Repository构造函数中.

IProductRepository ProductRepository = new ProductRepository(User);  //using IPrincipal
Run Code Online (Sandbox Code Playgroud)

如果您正在使用FormsAuthentication和自定义IUserToken,那么您可以围绕IPrincipal创建一个包装器,以便创建ProductRepository,如:

IProductRepository ProductRepository = new ProductRepository(new IUserTokenWrapper(User));
Run Code Online (Sandbox Code Playgroud)

现在,所有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
    );
}
Run Code Online (Sandbox Code Playgroud)

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

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

<% if(Model.CanEdit) { %>
  <a href="/Products/1/Edit">Edit</a>
<% } %>
Run Code Online (Sandbox Code Playgroud)

或在您的控制器中

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

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

重点是安全性存储在业务逻辑中而不是控制器中.