我应该在ASP.NET MVC站点中将访问权限的代码放在哪里?

时间:2011-05-18 00:26:55

标签: asp.net asp.net-mvc permissions access-control

我有一个ASP.NET MVC站点,它利用存储库模式来访问和修改数据。我的存储库接口通过它们的构造函数传递给每个控制器。我也在使用Ninject通过DependencyResolver.SetResolver()注入我的具体存储库类型。

我网站上的用户应该只能访问分配给他们的数据。我想弄清楚的是,我应该在哪里检查当前用户是否有权执行当前请求?

例如,用户可以向URL / Item / Delete / 123提交删除项目确认表单,这将删除ID为123的项目。但是,我不希望用户能够操作此URL和最终删除另一个用户的项目。

我应该将用户验证代码添加到控制器中,这样每个操作方法所做的第一件事就是检查当前用户是否拥有他们试图修改的数据?这似乎会给控制器增加太多的智能,这应该是相当薄的。

我认为将此逻辑添加到我的存储库会更有意义吗?例如,我可能有一个Repository.GetItem(int id,string user)而不是Repository.GetItem(int id),除非“user”拥有所请求的项目,否则它将抛出异常。

或者我认为我的存储库的每个实例都可以在实例化时分配给特定用户。如果曾尝试访问或修改当前用户不拥有的数据,则这些特定于用户的存储库将抛出异常。然后,控制器只需要捕获这些异常并将用户重定向到错误页面(如果有人被捕获)。

1 个答案:

答案 0 :(得分:5)

我最近遇到了同样的问题。我最终使用了继承自AuthorizeAttribute

的自定义ActionFilter

它基本上具有与Authorize相同的功能(检查用户是否属于至少一个列出的角色),还添加了检查用户是否“拥有”特定数据的功能。

以下是您用作示例的代码。如果有任何不清楚的地方,请发表评论,我会尝试解释。

[修改 - 根据Ryan的建议,我将params UserRole[]作为构造函数参数而不是公共属性,并添加了AllowAnyRolesIfNoneSpecified。]

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class AccountAuthorizeAttribute : AuthorizeAttribute
{
    private readonly UserRole[] _userRoles;

    public bool MustBeInRoleOrPageOwner { get; set; }
    public bool MustBeInRoleAndPageOwner { get; set; }
    public bool MustBeInRoleAndNotPageOwner { get; set; }
    public bool AllowAnyRolesIfNoneSpecified { get; set; }
    public string AccessDeniedViewName { get; set; }

    public AccountAuthorizeAttribute(params UserRole[] userRoles)
    {
        _userRoles = userRoles;
        MustBeInRoleOrPageOwner = false;
        MustBeInRoleAndPageOwner = false;
        MustBeInRoleAndNotPageOwner = false;
        AllowAnyRolesIfNoneSpecified = true;
        AccessDeniedViewName = "AccessDenied";
    }

    protected void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
        validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
        {
            ShowLogOnPage(filterContext);
            return;
        }
        using (var dbContext = new MainDbContext())
        {
            var accountService = new AccountService(dbContext);
            var emailAddress = filterContext.HttpContext.User.Identity.Name;
            if (IsUserInRole(accountService, emailAddress))
            {
                var isPageOwner = IsUserPageOwner(filterContext, dbContext, accountService, emailAddress);
                if (MustBeInRoleAndPageOwner && !isPageOwner || MustBeInRoleAndNotPageOwner && isPageOwner)
                    ShowAccessDeniedPage(filterContext);
            }
            else
            {
                if (!MustBeInRoleOrPageOwner)
                    ShowAccessDeniedPage(filterContext);
                else if (!IsUserPageOwner(filterContext, dbContext, accountService, emailAddress))
                    ShowAccessDeniedPage(filterContext);
            }
        }
    }

    private bool IsUserInRole(AccountService accountService, string emailAddress)
    {
        if (_userRoles.Length == 0 && AllowAnyRolesIfNoneSpecified) return true;
        return accountService.IsUserInRole(emailAddress, _userRoles);
    }

    protected virtual bool IsUserPageOwner(
        AuthorizationContext filterContext, MainDbContext dbContext, AccountService accountService, string emailAddress)
    {
        var id = GetRouteId(filterContext);
        return IsUserPageOwner(dbContext, accountService, emailAddress, id);
    }

    protected int GetRouteId(AuthorizationContext filterContext)
    {
        return Convert.ToInt32(filterContext.RouteData.Values["id"]);
    }

    private bool IsUserPageOwner(MainDbContext dbContext, AccountService accountService, string emailAddress, int id)
    {
        return accountService.IsUserPageOwner(emailAddress, id);
    }

    private void ShowLogOnPage(AuthorizationContext filterContext)
    {
        filterContext.Result = new HttpUnauthorizedResult();
    }

    private void ShowAccessDeniedPage(AuthorizationContext filterContext)
    {
        filterContext.Result = new ViewResult { ViewName = "ErrorPages/" + AccessDeniedViewName };
    }

    private void PreventPageFromBeingCached(AuthorizationContext filterContext)
    {
        var cachePolicy = filterContext.HttpContext.Response.Cache;
        cachePolicy.SetProxyMaxAge(new TimeSpan(0));
        cachePolicy.AddValidationCallback(CacheValidateHandler, null);
    }
}

一对夫妇注意到:

为了避免“魔术字符串”,我使用了UserRole枚举值的数组而不是单个字符串。另外,我构建它来处理我遇到的几种情况:

  • 用户必须是的角色/页面/数据的所有者(例如,管理员可以编辑任何人的数据,任何人都可以编辑自己的数据)
  • 用户必须处于角色页面/数据的所有者(例如,用户只能编辑他/她自己的页面/数据 - 通常在没有任何角色限制的情况下使用)
  • 用户必须处于角色中且是页面/数据的所有者(例如,管理员可以编辑除他自己以外的任何人的页面/数据 - 比如说,以防止管理员删除他的自己的帐户)
  • 不允许用户查看此页面AllowAnyRolesIfNoneSpecified = false(例如,您有一个不存在的页面的控制器方法,但您需要包含该方法,因为您的控制器继承自具有的基类这个方法)

这是一个示例属性声明:

[AccountAuthorize(UserRole.Admin, MustBeInRoleAndNotPageOwner = true)]
public override ActionResult DeleteConfirmed(int id)
{
    ...
}

(这意味着管理员可以删除任何帐户,但他自己的帐户。)