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

Asp.net mvc ASP.NET MVC中的访问控制取决于输入参数/服务层?,asp.net-mvc,Asp.net Mvc,序言:这是一个哲学问题。我更多的是寻找“正确”的方法,而不是“一种”的方法 假设我有一些产品,一个ASP.NET MVC应用程序在这些产品上执行CRUD:- mysite.example/products/1 mysite.example/products/1/edit 我使用的是存储库模式,因此这些产品来自何处并不重要:- public interface IProductRepository { IEnumberable<Product> GetProducts();

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

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

mysite.example/products/1
mysite.example/products/1/edit
我使用的是存储库模式,因此这些产品来自何处并不重要:-

public interface IProductRepository
{
  IEnumberable<Product> GetProducts();
  ....
}
我的问题:

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

提前谢谢

编辑

只是在这里补充一点细节。我遇到的问题是,我希望业务规则“只有具有权限的用户才能编辑产品”包含在一个且只有一个位置。我觉得决定用户是否可以获取或发布编辑操作的代码也应该负责决定是否在索引或详细信息视图上呈现“编辑”链接。也许那不可能/不可行,但我觉得应该

编辑2

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

回答我自己的问题(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方法和视图,尽管这在技术上是一个单独的业务规则)


将逻辑拉到某种权限仲裁器对象中(如我上面所做的)是否达到了最佳状态?

首先,我认为您已经理解了一半,因为您已经说过了

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

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

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

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

我认为这样的场景最好由一个封装ACL逻辑的类来建模,该类允许您查询决策或基于当前上下文进行断言—类似这样:

var p = this.ProductRepository.GetProductById(id);
var user = this.GetUser();
var permission = new ProductEditPermission(p);
public class Product
{
    public IEnumerable<ProductAccessRule> AccessRules { get; }

    // other members...
}
if (...)
{
    Response.StatusCode = 401;
    Response.StatusDescription = "Unauthorized";
    HttpContext.Response.End();
}
如果您只想知道用户是否可以编辑产品,可以发出查询:

bool canEdit = permission.IsGrantedTo(user);
如果您只想确保用户有权继续,可以发出断言:

permission.Demand(user);
如果未授予权限,则应引发异常

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

var p = this.ProductRepository.GetProductById(id);
var user = this.GetUser();
var permission = new ProductEditPermission(p);
public class Product
{
    public IEnumerable<ProductAccessRule> AccessRules { get; }

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


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

您的思路是正确的,但您可以将所有权限检查封装到一个方法中,如
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);
  }
}
enum权限
{
禁止=0,
访问权限=1,
管理员=2
}
公共类productBankedenException:异常
{ }
公共类产品控制器
{
公共产品GetProductForUser(int id、用户u、权限perm)
{
产品p=ProductRepository.GetProductById(id);
if(ProductPermissionService.UserPermission(u,p)public interface IUserToken {
  public int Uid { get; }
  public bool IsInRole(string role);
}
IProductRepository ProductRepository = new ProductRepository(User);  //using IPrincipal
IProductRepository ProductRepository = new ProductRepository(new IUserTokenWrapper(User));
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
    );
}
<% 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");
  }
}
[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;
    }
}