【问题标题】:Where should I put code for access permisions in my ASP.NET MVC Site?我应该将访问权限的代码放在我的 ASP.NET MVC 站点中的什么位置?
【发布时间】:2011-08-27 16:11:12
【问题描述】:

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

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

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

我是否应该将用户验证代码添加到控制器,以便每个操作方法首先检查当前用户是否拥有他们尝试修改的数据?这似乎会给控制器增加太多智能,而控制器应该相当薄。

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

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

【问题讨论】:

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


    【解决方案1】:

    我最近遇到了完全相同的问题。我最终选择了一个继承自 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)
    {
        ...
    }
    

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

    【讨论】:

    • +1。只是一个建议。您可以在构造函数中使用 params UserRole[] roles 来摆脱使用示例中糟糕的数组语法。
    • @Ryan,很好的建议。当我第一次编写该代码时,我对丑陋的语法感到沮丧,但当时我没有意识到您可以在属性声明中混合构造函数参数和属性。它变成你可以,结果更干净。我刚刚将新代码粘贴到我的答案中。谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-20
    • 2012-07-09
    • 2019-03-18
    相关资源
    最近更新 更多