关于MVC3使用重写AuthorizeAttribute、线程安全、子操作和缓存的自定义安全设置的问题

时间:2022-05-15 21:00:12

So after searching for a robust security solution for my MVC3 app, I came across this blog post by Rick Anderson. It details a WhiteList approach where a custom implementation of AuthorizeAttribute is applied as a Global Filter, and you decorate actions/controllers you wish to allow Anonymous access to using a dummy attribute AllowAnonymousAttribute (I say dummy because there is no logic inside of AllowAnonymousAttribute, it's just an empty attribute class)

因此,在为我的MVC3应用寻找一个健壮的安全解决方案之后,我偶然发现了Rick Anderson的这篇博文。它详细描述了一种白名单方法,在这种方法中,AuthorizeAttribute的自定义实现作为全局筛选器应用,您修饰了您希望允许匿名访问的动作/控制器,使用一个假属性allow来历(我说哑,是因为在allowaccounousattribute内部没有逻辑,它只是一个空属性类)

bool allowAnnonymous = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)
|| filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true);
if (allowAnnonymous) return;

This (along with other recommendations for security mentioned on his blog like HTTPS) gives me a secure by default model whereby I don't have to apply a security check to every single action, and remember to also add it to future feature additions.

这(连同他博客上提到的其他安全建议)给了我一个安全的默认模型,我不必对每一个动作都使用安全检查,而且还记得把它添加到将来的功能添加中。

First Part of Question

第一部分的问题

Now, I'm not using the Users/Roles properties on the AuthorizeAttribute, I need to grab that stuff from a database. To me that's something that would be in AuthorizeCore, since it's sole responsability is to return a true false, does the user have access. However I have a problem, AuthorizeCore has to be thread safe based off my reading of the source for the AuthorizeAttribute class, and I am not sure the best way to go about accessing my database to determine user permissions and adhere to that. My app is using IoC and currently letting my IoC container inject my repository handling all that to the constructor of the AuthorizeAttribute, but by doing this and then accessing it within AuthorizeCore, am I not causing problems with thread safety? Or will an IoC implementation and the MVC3 DependencyResolver I'm using to provide the parameter to my custom AuthorizeAttribute constructor handle the thread safety adequately? Note, my repositories are using a UnitOfWork pattern that includes my nHibernate SessionFactory as a contructor to the repository and the Unit of Work class is provided from my IoC container, implemented by StructureMap using the line below, am I correct in thinking the scope used here would handle threading concerns?

现在,我没有使用authorize属性上的Users/Roles属性,我需要从数据库中获取这些内容。对我来说,这是授权的事情,因为它的唯一响应是返回一个真正的错误,用户是否有访问权限。但是我有一个问题,AuthorizeCore必须是线程安全的,这是基于我对AuthorizeAttribute类源代码的读取,我不确定访问数据库的最佳方式,以确定用户权限并遵守该方法。我的应用程序正在使用IoC,目前允许IoC容器将处理所有这些的存储库注入到authorize属性的构造函数中,但这样做然后在AuthorizeCore中访问它们,难道我没有造成线程安全性问题吗?或者IoC实现和MVC3依赖于我用来向我的自定义AuthorizeAttribute构造函数提供参数的解析器是否能够充分地处理线程安全性?注意,我使用UnitOfWork模式存储库,其中包括nHibernate SessionFactory作为contructor存储库和工作单元类提供了从我的IoC容器,由StructureMap实现使用下面的线,我这里使用正确的思考范围处理线程问题吗?

For<IUnitOfWork>().HybridHttpOrThreadLocalScoped().Use<UnitOfWork>();

Second Part of Question

第二部分的问题

My data model (and thus security model) is setup so that my primary business objects are all defined is such a way that it's one large hierarchy model, and that when I check for permissions I look in that hierarchy model for where the user's account was defined and grant access to everything underneath it by default. The secondary permissions check is the one that uses Administrative defined business logic permissions like can users in role X access the Delete Widget functionality. For this I'm using the Route data and pulling out the Controller and Action names, and use them in conjunction with details from the current users Principal details to hit my database to resolve the permissions for this request. However this logic is being repeated for each ChildAction used on the page as well, but because I'm using the Controller and Action names from the Route data, I'm not actually getting the Child Action information. It stays as the parent action name, not the child action since the child action is not being executed via a URL request. This is causes redundant security checks on my database for the details of the Parent Action and needless resource hits. In researching this I decided to simply bypass the security check for Child actions and rely on the parent action for this.

我的数据模型(因此安全模型)的设置,这样我的主要业务对象的定义都是这样的一个大层次结构模型中,当我检查权限层次结构模型,用户的账户被定义和授权访问默认下面的一切。次要权限检查是使用管理定义的业务逻辑权限的检查,如角色X中的用户可以访问Delete小部件功能。为此,我将使用路由数据并提取控制器和操作名,并结合当前用户主体详细信息使用它们来访问数据库,以解析此请求的权限。然而,对于页面上使用的每个子动作,这个逻辑也在重复,但是因为我使用了路由数据中的控制器和动作名,所以实际上我并没有获得子动作信息。它作为父操作名而不是子操作,因为子操作没有通过URL请求执行。这会导致对我的数据库进行冗余安全检查,以获取父操作的细节和不必要的资源命中。在研究这个问题时,我决定简单地绕过子动作的安全检查,并依赖于父动作。

bool bypassChildAction = filterContext.ActionDescriptor.IsDefined(typeof (ChildActionOnlyAttribute), true) || filterContext.IsChildAction;
if (bypassChildAction) return;

Does it make sense to do this, and if so/not, why? In my mind, if the Action is decorated with ChildActionOnlyAttribute, it's inaccessible publicly via a URL anyway. And if it's being executed as a Child Action but is not exclusively a child action, I can bypass the security check just for this execution since the parent action will handle the permissions. Would you ever have a situation where you would need restrict access to a child action? Knowing that child actions are typically very small partial views I don't anticipate this being a problem but I also saw reference to a line in the default implementation of OnAuthorization outlining some concerns over caching. Does anyone know if that affects my proposed solution?

这样做有意义吗?如果有,为什么?在我看来,如果操作用ChildActionOnlyAttribute修饰,那么无论如何它都可以通过URL公开访问。如果它是作为子操作执行的,但并不是唯一的子操作,那么我可以绕过这个执行的安全检查,因为父操作将处理权限。您是否曾经遇到过需要限制访问子动作的情况?由于知道子操作通常是非常小的局部视图,所以我不认为这是一个问题,但我还看到了OnAuthorization默认实现中的一行引用,概述了对缓存的一些关注。有人知道这是否会影响我提出的解决方案吗?

Summary concerns:

总结问题:

  • Multi-threading concerns for accessing user permission from database within AuthorizeCore
  • 多线程的关注点,用于访问权限内的数据库的用户权限。
  • Security concerns over bypassing Authorization check for child actions
  • 安全考虑绕过授权检查子行为。
  • Caching concerns for Child Actions combines with previous point
  • 将子动作的缓存关注点与前一点结合起来

Any opinions or help with these aspects would be greatly appreciated!

如有任何意见或帮助,我们将不胜感激!

2 个解决方案

#1


2  

Heya Yarx - Part 1- cache all permissions for the user upon login. Multi threaded access then is not an issue as your AuthorizeCore simply gets the roles from the cache which at that time can be considered read only.

Heya Yarx -第1部分-在登录时缓存用户的所有权限。因此多线程访问并不是问题,因为您的AuthorizeCore只是从缓存中获取角色,而在当时,这些角色只能被视为只读。

Part 2: Again going to point 1 above : ) - if your security checks are so heavy, why not load all permissions for a user upon login and cache them. Upon hitting your child actions you can demand the permissions and at that time check the cache for them.

第2部分:再次指向上面的第1点:)——如果您的安全检查非常重,为什么不为用户加载所有的权限,然后登录并缓存它们。在执行子动作时,您可以要求权限,并在此期间检查缓存。

There is definitely a better way to handle this that isn't as heavy. If you are hitting the db multiple times in a single request solely for permissions, you need to either cache your permission set via some mechanism (custom or implementing another claims based system, etc)

肯定有更好的方法来处理这个不那么重的问题。如果您在单个请求中仅为获得权限而多次访问db,您需要通过某种机制(自定义或实现另一个基于索赔的系统等)缓存您的权限集

I'm not 100% following your mechanism though for authorization based on the route. You mentioned that you are pulling info from the route - can you give an example here?

我不是百分之百地遵循你的机制,但是基于路线的授权。你提到你正在从路线中提取信息-你能举一个例子吗?

It absolutely makes sense to protect your child actions. What if two views call Html.Action - one specifically as an admin and the other is erroneously copy and pasted into another view? Your child actions should always be protected, don't assume they are ok since they are only called from another view.

保护孩子的行为绝对是有意义的。如果两个视图调用Html呢?操作——一个是作为管理员,另一个是错误地复制粘贴到另一个视图中?您的子动作应该始终受到保护,不要假设它们是ok的,因为它们只从另一个视图调用。

Also if you cannot cache the entire tree for a user, you can certainly cache the security checks in the first call to AuthorizeCore. Subsequent calls would simply check for ex. cached roles - if there then it uses them, otherwise look to the database.

此外,如果不能为用户缓存整个树,那么可以在第一次调用AuthorizeCore时缓存安全性检查。随后的调用将简单地检查ex.缓存的角色——如果有的话,它将使用它们,否则将查看数据库。

#2


0  

There is where I'm at so far. I feel like I've bastardized it but I'm not sure how else to do it with the requirements I'm looking to satisfy. Am I going about this all wrong or is it just in my head?

这是我目前所处的位置。我觉得我已经把它搞砸了,但我不知道还有什么方法可以满足我的要求。我说的全是错的,还是只是在我脑子里想的?

public class LogonAuthorize : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }

        if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
        {
            // If a child action cache block is active, we need to fail immediately, even if authorization
            // would have succeeded. The reason is that there's no way to hook a callback to rerun
            // authorization before the fragment is served from the cache, so we can't guarantee that this
            // filter will be re-run on subsequent requests.
            throw new InvalidOperationException("AuthorizeAttribute cannot be used within a child action caching block."); //Text pulled from System.Web.Mvc.Resources
        }
        // Bypass authorization on any action decorated with AllowAnonymousAttribute, indicationg the page allows anonymous access and
        // does not restrict access anyone (Similar to a WhiteList security model).
        bool allowAnnonymous = filterContext.ActionDescriptor.IsDefined(typeof (AllowAnonymousAttribute), true) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof (AllowAnonymousAttribute), true);
        if (allowAnnonymous) return;

        if (CustomAuthorizeCore(filterContext))
        {
            HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
            cachePolicy.SetProxyMaxAge(new TimeSpan(0));
            cachePolicy.AddValidationCallback(CacheValidateHandler, filterContext); //CacheValidateHandler doesn't have access to our AuthorizationContext, so we pass it in using the data object.
        }

        HandleUnauthorizedRequest(filterContext);
    }

    private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
        var filterContext = (AuthorizationContext)data;
        validationStatus = CustomOnCacheAuthorization(filterContext);
    }

    protected HttpValidationStatus CustomOnCacheAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext == null)
        {
            throw new ArgumentNullException("filterContext.HttpContext");
        }

        bool isAuthorized = CustomAuthorizeCore(filterContext);
        return (isAuthorized) ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
    }

    protected bool CustomAuthorizeCore(AuthorizationContext filterContext)
    {
        HttpContextBase httpContext = filterContext.HttpContext;

        if (httpContext == null)
            throw new ArgumentNullException("filterContext.HttpContext");

        Trace.WriteLine("Current User: " + (httpContext.User.Identity.IsAuthenticated ? httpContext.User.Identity.Name : "Anonymous"));
        if (!httpContext.User.Identity.IsAuthenticated)
            return false;

        string objectId = (httpContext.Request.RequestContext.RouteData.Values["id"] ?? Guid.Empty).ToString();
        Trace.WriteLine("Hierarchy Permissions check for Object: " + objectId);

        string controllerName = httpContext.Request.RequestContext.RouteData.GetRequiredString("controller");
        string actionName = httpContext.Request.RequestContext.RouteData.GetRequiredString("action");
        Trace.WriteLine("Policy Permissions check for Controller: " + controllerName + ", and Action: " + actionName);
        //if(!CheckHierarchyPermissions  || (!CheckHierarchyPermissions && !CheckBusinessLogicPermissions))
        //{
        //    //Check database permissions by getting DB reference from DependancyResolver
        //    DependencyResolver.Current.GetService(typeof (SecurityService)); //change this to an interface later
        //    return false;
        //}

        return true;
    }

    #region Old methods decorated with Obsolete() attributes to track down unintended uses
    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the Users collection.", true)]
    public new string Users { get; set; }

    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the Roles collection.", true)]
    public new string Roles { get; set; }

    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the AuthorizeCore method.", true)]
    protected new bool AuthorizeCore(HttpContextBase httpContext)
    {
        return false;
    }

    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the OnCacheAuthorization method.", true)]
    protected new virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
    {
        return HttpValidationStatus.Invalid;
    }
    #endregion
}

UPDATE: Just a quick update on this one, I never did find a way to dynamically build the name of the Role I was checking for from the combination of Action name and Controller name, and still work within the limitations of the way the requests are made and caching, etc. However the pattern of the WhiteList approach to authorization as detailed on the Blog I linked above is included in MVC4. MVC4 is beta only at the moment, but I don't expect they'll remove it between now and the final version.

更新:快速更新在这一点上,我没有找到一个方法来动态地构建角色的名字我检查相结合的操作名称和控制器名称,还是内工作的局限性和缓存的请求,等等。然而白名单方式授权的模式详细上面的博客链接包含在MVC4。MVC4目前只是测试版,但我不认为他们会在现在和最终版本之间删除它。

#1


2  

Heya Yarx - Part 1- cache all permissions for the user upon login. Multi threaded access then is not an issue as your AuthorizeCore simply gets the roles from the cache which at that time can be considered read only.

Heya Yarx -第1部分-在登录时缓存用户的所有权限。因此多线程访问并不是问题,因为您的AuthorizeCore只是从缓存中获取角色,而在当时,这些角色只能被视为只读。

Part 2: Again going to point 1 above : ) - if your security checks are so heavy, why not load all permissions for a user upon login and cache them. Upon hitting your child actions you can demand the permissions and at that time check the cache for them.

第2部分:再次指向上面的第1点:)——如果您的安全检查非常重,为什么不为用户加载所有的权限,然后登录并缓存它们。在执行子动作时,您可以要求权限,并在此期间检查缓存。

There is definitely a better way to handle this that isn't as heavy. If you are hitting the db multiple times in a single request solely for permissions, you need to either cache your permission set via some mechanism (custom or implementing another claims based system, etc)

肯定有更好的方法来处理这个不那么重的问题。如果您在单个请求中仅为获得权限而多次访问db,您需要通过某种机制(自定义或实现另一个基于索赔的系统等)缓存您的权限集

I'm not 100% following your mechanism though for authorization based on the route. You mentioned that you are pulling info from the route - can you give an example here?

我不是百分之百地遵循你的机制,但是基于路线的授权。你提到你正在从路线中提取信息-你能举一个例子吗?

It absolutely makes sense to protect your child actions. What if two views call Html.Action - one specifically as an admin and the other is erroneously copy and pasted into another view? Your child actions should always be protected, don't assume they are ok since they are only called from another view.

保护孩子的行为绝对是有意义的。如果两个视图调用Html呢?操作——一个是作为管理员,另一个是错误地复制粘贴到另一个视图中?您的子动作应该始终受到保护,不要假设它们是ok的,因为它们只从另一个视图调用。

Also if you cannot cache the entire tree for a user, you can certainly cache the security checks in the first call to AuthorizeCore. Subsequent calls would simply check for ex. cached roles - if there then it uses them, otherwise look to the database.

此外,如果不能为用户缓存整个树,那么可以在第一次调用AuthorizeCore时缓存安全性检查。随后的调用将简单地检查ex.缓存的角色——如果有的话,它将使用它们,否则将查看数据库。

#2


0  

There is where I'm at so far. I feel like I've bastardized it but I'm not sure how else to do it with the requirements I'm looking to satisfy. Am I going about this all wrong or is it just in my head?

这是我目前所处的位置。我觉得我已经把它搞砸了,但我不知道还有什么方法可以满足我的要求。我说的全是错的,还是只是在我脑子里想的?

public class LogonAuthorize : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }

        if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
        {
            // If a child action cache block is active, we need to fail immediately, even if authorization
            // would have succeeded. The reason is that there's no way to hook a callback to rerun
            // authorization before the fragment is served from the cache, so we can't guarantee that this
            // filter will be re-run on subsequent requests.
            throw new InvalidOperationException("AuthorizeAttribute cannot be used within a child action caching block."); //Text pulled from System.Web.Mvc.Resources
        }
        // Bypass authorization on any action decorated with AllowAnonymousAttribute, indicationg the page allows anonymous access and
        // does not restrict access anyone (Similar to a WhiteList security model).
        bool allowAnnonymous = filterContext.ActionDescriptor.IsDefined(typeof (AllowAnonymousAttribute), true) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof (AllowAnonymousAttribute), true);
        if (allowAnnonymous) return;

        if (CustomAuthorizeCore(filterContext))
        {
            HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
            cachePolicy.SetProxyMaxAge(new TimeSpan(0));
            cachePolicy.AddValidationCallback(CacheValidateHandler, filterContext); //CacheValidateHandler doesn't have access to our AuthorizationContext, so we pass it in using the data object.
        }

        HandleUnauthorizedRequest(filterContext);
    }

    private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
        var filterContext = (AuthorizationContext)data;
        validationStatus = CustomOnCacheAuthorization(filterContext);
    }

    protected HttpValidationStatus CustomOnCacheAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext == null)
        {
            throw new ArgumentNullException("filterContext.HttpContext");
        }

        bool isAuthorized = CustomAuthorizeCore(filterContext);
        return (isAuthorized) ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
    }

    protected bool CustomAuthorizeCore(AuthorizationContext filterContext)
    {
        HttpContextBase httpContext = filterContext.HttpContext;

        if (httpContext == null)
            throw new ArgumentNullException("filterContext.HttpContext");

        Trace.WriteLine("Current User: " + (httpContext.User.Identity.IsAuthenticated ? httpContext.User.Identity.Name : "Anonymous"));
        if (!httpContext.User.Identity.IsAuthenticated)
            return false;

        string objectId = (httpContext.Request.RequestContext.RouteData.Values["id"] ?? Guid.Empty).ToString();
        Trace.WriteLine("Hierarchy Permissions check for Object: " + objectId);

        string controllerName = httpContext.Request.RequestContext.RouteData.GetRequiredString("controller");
        string actionName = httpContext.Request.RequestContext.RouteData.GetRequiredString("action");
        Trace.WriteLine("Policy Permissions check for Controller: " + controllerName + ", and Action: " + actionName);
        //if(!CheckHierarchyPermissions  || (!CheckHierarchyPermissions && !CheckBusinessLogicPermissions))
        //{
        //    //Check database permissions by getting DB reference from DependancyResolver
        //    DependencyResolver.Current.GetService(typeof (SecurityService)); //change this to an interface later
        //    return false;
        //}

        return true;
    }

    #region Old methods decorated with Obsolete() attributes to track down unintended uses
    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the Users collection.", true)]
    public new string Users { get; set; }

    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the Roles collection.", true)]
    public new string Roles { get; set; }

    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the AuthorizeCore method.", true)]
    protected new bool AuthorizeCore(HttpContextBase httpContext)
    {
        return false;
    }

    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the OnCacheAuthorization method.", true)]
    protected new virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
    {
        return HttpValidationStatus.Invalid;
    }
    #endregion
}

UPDATE: Just a quick update on this one, I never did find a way to dynamically build the name of the Role I was checking for from the combination of Action name and Controller name, and still work within the limitations of the way the requests are made and caching, etc. However the pattern of the WhiteList approach to authorization as detailed on the Blog I linked above is included in MVC4. MVC4 is beta only at the moment, but I don't expect they'll remove it between now and the final version.

更新:快速更新在这一点上,我没有找到一个方法来动态地构建角色的名字我检查相结合的操作名称和控制器名称,还是内工作的局限性和缓存的请求,等等。然而白名单方式授权的模式详细上面的博客链接包含在MVC4。MVC4目前只是测试版,但我不认为他们会在现在和最终版本之间删除它。