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

Eri*_*tas 1 asp.net permissions asp.net-mvc 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"拥有所请求的项目,否则它将抛出异常.

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

dev*_*xer 5

我最近遇到了完全相同的问题.我最终使用继承自的自定义ActionFilter AuthorizeAttribute.

它基本上具有与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);
    }
}
Run Code Online (Sandbox Code Playgroud)

几个笔记:

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

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

这是一个示例属性声明:

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

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

  • +1.只是一个建议.您可以在构造函数中使用`params UserRole [] roles`来摆脱使用示例中的可怕数组语法. (2认同)