Why can't I combine [Authorize] and [OutputCache] attributes when using Azure cache (.NET MVC3 app)?

女生的网名这么多〃 提交于 2019-11-27 08:03:48
one.beat.consumer

Caching happens before the Action. You will likely need to customize your authorization mechanics to handle cache scenarios.

Check out a question I posted a while back - MVC Custom Authentication, Authorization, and Roles Implementation.

The part I think would help you is a custom Authorize Attribute who's OnAuthorize() method deals with caching.

Below is a code block for example:

/// <summary>
/// Uses injected authorization service to determine if the session user 
/// has necessary role privileges.
/// </summary>
/// <remarks>As authorization code runs at the action level, after the 
/// caching module, our authorization code is hooked into the caching 
/// mechanics, to ensure unauthorized users are not served up a 
/// prior-authorized page. 
/// Note: Special thanks to TheCloudlessSky on StackOverflow.
/// </remarks>
public void OnAuthorization(AuthorizationContext filterContext)
{
    // User must be authenticated and Session not be null
    if (!filterContext.HttpContext.User.Identity.IsAuthenticated || filterContext.HttpContext.Session == null)
        HandleUnauthorizedRequest(filterContext);
    else {
        // if authorized, handle cache validation
        if (_authorizationService.IsAuthorized((UserSessionInfoViewModel)filterContext.HttpContext.Session["user"], _authorizedRoles)) {
            var cache = filterContext.HttpContext.Response.Cache;
            cache.SetProxyMaxAge(new TimeSpan(0));
            cache.AddValidationCallback((HttpContext context, object o, ref HttpValidationStatus status) => AuthorizeCache(context), null);
        }
        else
            HandleUnauthorizedRequest(filterContext);             
    }
}

/// <summary>
/// Ensures that authorization is checked on cached pages.
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public HttpValidationStatus AuthorizeCache(HttpContext httpContext)
{
    if (httpContext.Session == null)
        return HttpValidationStatus.Invalid;
    return _authorizationService.IsAuthorized((UserSessionInfoViewModel) httpContext.Session["user"], _authorizedRoles) 
        ? HttpValidationStatus.Valid 
        : HttpValidationStatus.IgnoreThisRequest;
}

I've come back to this issue and, after a bit of tinkering, have concluded that you cannot use the out of the box System.Web.Mvc.AuthorizeAttribute along with the out of the box System.Web.Mvc.OutputCacheAttribute when using the Azure DistributedCache. The main reason is because, as the error message in the original question states, the validation callback method must be static in order to use it with Azure's DistributedCache. The cache callback method in the MVC Authorize attribute is an instance method.

I went about trying to figure out how to make it work by making a copy of the AuthorizeAttribute from the MVC source, renaming it, hooking it up to an action with OutputCache connected to Azure, and debugging. The reason the cache callback method is not static is because, in order to authorize, the attribute needs to check the HttpContext's User against the Users and Roles property values that are set when the attribute is constructed. Here is the relevant code:

OnAuthorization

public virtual void OnAuthorization(AuthorizationContext filterContext) {
    //... code to check argument and child action cache

    if (AuthorizeCore(filterContext.HttpContext)) {
        // Since we're performing authorization at the action level, 
        // the authorization code runs after the output caching module. 
        // In the worst case this could allow an authorized user
        // to cause the page to be cached, then an unauthorized user would 
        // later be served the cached page. We work around this by telling 
        // proxies not to cache the sensitive page, then we hook our custom
        // authorization code into the caching mechanism so that we have
        // the final say on whether a page should be served from the cache.

        HttpCachePolicyBase cachePolicy = filterContext
            .HttpContext.Response.Cache;
        cachePolicy.SetProxyMaxAge(new TimeSpan(0));
        cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
    }
    else {
        HandleUnauthorizedRequest(filterContext);
    }
}

Cache Validation Callback

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

// This method must be thread-safe since it is called by the caching module.
protected virtual HttpValidationStatus OnCacheAuthorization
    (HttpContextBase httpContext) {
    if (httpContext == null) {
        throw new ArgumentNullException("httpContext");
    }

    bool isAuthorized = AuthorizeCore(httpContext);
    return (isAuthorized) 
        ? HttpValidationStatus.Valid 
        : HttpValidationStatus.IgnoreThisRequest;
}

As you can see, the cache validation callback ultimately invokes AuthorizeCore, which is another instance method (protected virtual). AuthorizeCore, which was also called during OnAuthorization, does 3 main things:

  1. Checks that the HttpContextBase.User.Identity.IsAuthenticated == true

  2. If the attribute has a non-empty Users string property, checks that the HttpContextBase.User.Identity.Name matches one of the comma-separated values.

  3. If the attribute has a non-empty Roles string property, checks that the HttpContextBase.User.IsInRole for one of the comma-separated values.

AuthorizeCore

// This method must be thread-safe since it is called by the thread-safe
// OnCacheAuthorization() method.
protected virtual bool AuthorizeCore(HttpContextBase httpContext) {
    if (httpContext == null) {
        throw new ArgumentNullException("httpContext");
    }

    IPrincipal user = httpContext.User;
    if (!user.Identity.IsAuthenticated) {
        return false;
    }

    if (_usersSplit.Length > 0 && !_usersSplit.Contains
        (user.Identity.Name, StringComparer.OrdinalIgnoreCase)) {
        return false;
    }

    if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole)) {
         return false;
    }

    return true;
}

When you simply try to make the validation callback method static, the code won't compile because it needs access to these _rolesSplit and _usersSplit fields, which are based on the public Users and Roles properties.

My first attempt was to pass these values to the callback using the object data argument of the CacheValidateHandler. Even after introducing static methods, this still did not work, and resulted in the same exception. I was hoping that the object data would be serialized, then passed back to the validate handler during the callback. Apparently this is not the case, and when you try to do this, Azure's DistributedCache still considers it a non-static callback, resulting in the same exception & message.

// this won't work
cachePolicy.AddValidationCallback(CacheValidateHandler, new object() /* data */);

My second attempt was to add the values to the HttpContext.Items collection, since an instance of HttpContext is automatically passed to the handler. This didn't work either. The HttpContext that is passed to the CacheValidateHandler is not the same instance that existed on the filterContext.HttpContext property. In fact, when the CacheValidateHandler executes, it has a null Session and always has an empty Items collection.

// this won't work
private void CacheValidateHandler(HttpContext context, object data, 
    ref HttpValidationStatus validationStatus) {
    Debug.Assert(!context.Items.Any()); // even after I put items into it
    validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}

However...

Even though there seems to be no way to pass the Users & Roles property values back to the cache validation callback handler, the HttpContext passed to it does in fact have the correct User Principal. Also, none of the actions where I currently want to combine [Authorize] and [OutputCache] ever pass a Users or Roles property to the AuthorizeAttribute constructor.

So, it is possible to create a custom AuthenticateAttribute which ignores these properties, and only checks to make sure the User.Identity.IsAuthenticated == true. If you need to authenticate against a specific role, you could also do so and combine with OutputCache... however, you would need a distinct attribute for each (set of) Role(s) in order to make the cache validation callback method static. I will come back and post the code after I've polished it a bit.

You are correct olive. Caching works by caching the entire output of the Action (including all attributes) then returning the result to subsequent calls without actually calling any of your code.

Because of this you cannot cache and check authorization because by caching you are not going to call any of your code (including authorization). Therefore anything that is cached must be public.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!