为什么在使用Azure缓存(.NET MVC3应用程序)时,我不能组合使用[Authorize]和[OutputCache]属性?

22

在MVC3应用程序中使用Windows Azure的Microsoft.Web.DistributedCache.DistributedCacheOutputCacheProvider作为outputCache提供程序。下面是相关的Action方法:

[ActionName("sample-cached-page")]
[OutputCache(Duration = 300, VaryByCustom = "User", 
    Location = OutputCacheLocation.Server)]
[Authorize(Users = "me@mydomain.tld,another@otherdomain.tld")]
public virtual ActionResult SampleCachedPage()
{
    return View();
}
我在从网页浏览器中加载此视图时遇到以下异常:
System.Configuration.Provider.ProviderException: When using a custom output cache provider like 'DistributedCache', only the following expiration policies and cache features are supported: file dependencies, absolute expirations, static validation callbacks and static substitution callbacks.

System.Configuration.Provider.ProviderException: When using a custom output cache provider like 'DistributedCache', only the following expiration policies and cache features are supported:  file dependencies, absolute expirations, static validation callbacks and static substitution callbacks.
   at System.Web.Caching.OutputCache.InsertResponse(String cachedVaryKey, CachedVary cachedVary, String rawResponseKey, CachedRawResponse rawResponse, CacheDependency dependencies, DateTime absExp, TimeSpan slidingExp)
   at System.Web.Caching.OutputCacheModule.OnLeave(Object source, EventArgs eventArgs)
   at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
如果我移除 [Authorize] 属性,视图会按照预期进行缓存。这是否意味着我不能在必须具有 [Authorize] 的操作方法上放置 [OutputCache]?还是说,我需要使用静态验证回调方法覆盖 AuthorizeAttribute 的自定义实现来为缓存提供支持?

更新 1

在 Evan 的答案之后,我在 IIS Express(不在 Azure 中)中测试了上述操作方法。下面是我针对 OutputCache 属性的 VaryByCustom = "User" 属性的重写:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
    return "User".Equals(custom, StringComparison.OrdinalIgnoreCase)
        ? Thread.CurrentPrincipal.Identity.Name
        : base.GetVaryByCustomString(context, custom);
}
当我使用me@mydomain.tld访问示例缓存页面时,页面的输出被缓存,视图显示“This page was cached at 12/31/2011 11:06:12 AM (UTC)”。如果我退出并登录其他用户another@otherdomain.tld并访问页面,则它会显示“This page was cached at 12/31/2011 11:06:38 AM (UTC)”。再次登录me@mydomain.tld并重新访问页面会导致缓存再次显示“This page was cached at 12/31/2011 11:06:12 AM (UTC)” 。进一步的登录/注销尝试表明,不同的输出根据用户被缓存和返回。
这让我相信基于用户的输出被单独缓存,这是我使用VaryByCustom =“User”设置和覆盖的意图。问题在于它不能与Azure的分布式缓存提供程序一起使用。Evan,你有关仅缓存公共内容的答案是否仍然有效?
更新2
我找到了源代码,并发现开箱即用的AuthorizeAttribute确实具有非静态验证回调函数。以下是OnAuthorization的摘录:
if (AuthorizeCore(filterContext.HttpContext)) {
    // ** IMPORTANT **
    // 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);
}

CacheValidationHandler委托缓存验证给protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase),它显然不是静态的。其中一个原因是它调用了上面重要注释中提到的protected virtual bool AuthorizeCore(HttpContextBase)

为了从静态缓存验证回调方法执行任何AuthorizeCore逻辑,它需要知道AuthorizeAttribute实例的Users和Roles属性。但是似乎没有一个简单的插入方式。我必须覆盖OnAuthorization并将这两个值放入HttpContext(Items集合?),然后覆盖OnCacheAuthorization以将它们取回。但这听起来很糟糕。

如果我们小心使用OutputCache属性中的VaryByCustom = "User"属性,是否可以仅覆盖OnCacheAuthorization始终返回HttpValidationStatus.Valid呢?当操作方法没有OutputCache属性时,我们不需要担心此回调是否被调用,正确吗?如果我们有一个没有VaryByCustom = "User"的OutputCache属性,那么很明显页面可以返回任何缓存版本,而不管哪个用户请求创建了缓存副本。这有多冒险?


橄榄-除了我的下面的回答,你还可以看一下TheCloudlessSky的原始帖子,那是我在代码中得到灵感的地方...另外,抛弃任何不必要的有关注入服务或Sessions的东西...所有这些都是针对我特定情况的。重要的是以你需要的方式处理OnAuthorization()函数中的缓存验证。 :) 保重。 - one.beat.consumer
如果您使用“UseSlidingExpiration = False”来强制执行绝对过期,会发生什么? - lalibi
1
你知道这个问题是否仍然存在吗?我似乎在使用MVC5时遇到了这个问题,但除了这篇文章之外,似乎并不常见。真的很奇怪它为什么就不能工作。我无法想象使用缓存和Azure输出缓存是那么罕见的。 - GraemeMiller
@GraemeMiller,如果您正在使用旧的多租户Azure缓存,则是的,我认为这个问题仍然存在。他们没有改变Azure“分布式”缓存。但是现在有一种替代方法可以通过使用工作角色的内存来存储缓存内容进行缓存。我从未尝试过将授权与指向基于工作角色的缓存的outputcache相结合。请参见我在已接受答案中的最后一条评论,我能够通过自定义outputcache属性解决此问题。 - danludwig
3个回答

10

在Action之前会进行缓存。您可能需要自定义授权机制来处理缓存方案。

可以查看我一段时间前发布的一个问题 - MVC自定义身份验证、授权和角色实现

我认为有用的部分是自定义Authorize属性,其OnAuthorize()方法可以处理缓存。

下面是一个示例代码块:

/// <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;
}

这正是我想的。AuthorizeCache(context)是从哪里来的?它是一个静态方法吗?目前编译器报错“无法解析符号'AuthorizeCache'”。 - danludwig
这完全取决于你,它是静态的还是基于实例的以及它的功能...我的现在只是占位逻辑,但它向你展示了如何处理它的位置和方式... - one.beat.consumer
你是正确的,我需要一个自定义属性,专门处理Azure的缓存细节。 - danludwig
2
很奇怪这样变得如此复杂。我们所有的授权属性都需要根据角色进行检查,所以我认为我无法使其正常工作。你曾经使用Azure输出缓存提供程序实现基于角色的授权吗? - GraemeMiller
1
_authorizationService和UserSessionInfoViewModel从哪里来?为什么我不能使用filterContext.HttpContext.User.Identity.IsAuthenticated? - johnstaveley
显示剩余2条评论

7
我回到了这个问题,并经过一些调试,得出结论:当使用Azure分布式缓存时,您不能同时使用开箱即用的System.Web.Mvc.AuthorizeAttributeSystem.Web.Mvc.OutputCacheAttribute。主要原因是,正如原始问题中的错误消息所述,验证回调方法必须是静态的才能与Azure的分布式缓存一起使用。MVC授权属性中的缓存回调方法是实例方法。

我试图通过从MVC源代码中复制AuthorizeAttribute并将其重命名,将其连接到具有连接到Azure的OutputCache的操作,并进行调试来弄清楚如何使其工作。缓存回调方法不是静态的原因是,为了进行授权,属性需要检查HttpContext的用户与在构造属性时设置的Users和Roles属性值是否匹配。以下是相关代码:

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);
    }
}

缓存验证回调函数
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;
}

如您所见,缓存验证回调最终调用了AuthorizeCore方法,这是另一个实例方法(protected virtual)。AuthorizeCore方法也在OnAuthorization期间被调用,它主要执行以下三个操作:
  1. 检查HttpContextBase.User.Identity.IsAuthenticated == true
  2. 如果属性具有非空的Users字符串属性,则检查HttpContextBase.User.Identity.Name是否与逗号分隔的值之一匹配。
  3. 如果属性具有非空的Roles字符串属性,则检查HttpContextBase.User.IsInRole是否为逗号分隔的值之一。
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;
}

当您尝试将验证回调方法设置为静态时,代码无法编译,因为它需要访问基于公共用户和角色属性的_rolesSplit和_usersSplit字段。
我的第一次尝试是使用CacheValidateHandler的对象数据参数将这些值传递给回调。即使引入了静态方法,这仍然没有起作用,并导致相同的异常。我希望对象数据会被序列化,然后在回调期间传递回验证处理程序。显然,这不是情况,当您尝试这样做时,Azure的DistributedCache仍将其视为非静态回调,导致相同的异常和消息。
// this won't work
cachePolicy.AddValidationCallback(CacheValidateHandler, new object() /* data */);

我的第二次尝试是将值添加到HttpContext.Items集合中,因为HttpContext的实例会自动传递给处理程序。但这也没有起作用。传递给CacheValidateHandler的HttpContext与filterContext.HttpContext属性上存在的实例不是同一实例。实际上,当CacheValidateHandler执行时,它具有空Session并且始终具有空的Items集合。
// 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));
}

然而...
尽管似乎没有办法将用户和角色属性值传递回缓存验证回调处理程序,但传递给它的HttpContext实际上具有正确的用户主体。此外,我目前希望结合[Authorize]和[OutputCache]的操作中没有一个操作将Users或Roles属性传递给AuthorizeAttribute构造函数。
因此,可以创建一个自定义的AuthenticateAttribute,忽略这些属性,并仅检查User.Identity.IsAuthenticated == true以确保身份验证。如果需要对特定角色进行身份验证,也可以这样做并与OutputCache结合使用...但是,为了使缓存验证回调方法静态化,您需要为每个(组)角色设置一个不同的属性。稍微完善一下后,我会回来发布代码。

2

你是正确的,olive。缓存的工作原理是缓存Action的全部输出(包括所有属性),然后在不实际调用任何代码的情况下将结果返回给后续的调用。

因此,你不能对缓存进行授权检查,因为在缓存时不会调用任何代码(包括授权)。因此,任何被缓存的内容都必须是公共的。


1
我已经更新了我的问题。我能够成功地在IIS Express中缓存用户相关内容,使用内置的缓存。问题是在Azure中执行此操作。 - danludwig
我不知道您可以按用户变化,但是您说的在本地 IIS 中确实有效。Azure 使用 AppFabric 缓存而非本地缓存,但由于缓存代码在应用程序服务器上执行,我不确定为什么它不起作用。您是否覆盖了 DistributedCacheProvider 上相同的方法? - Evan
1
VaryByCustom重写位于global.asax中。DistributedOutputCacheProvider位于Microsoft.Web.DistributedCache dll中。我认为可以重写AuthorizeAttribute并调用filterContext.HttpContext.Cache.AddValidationCallback。但是对于Azure缓存,处理程序参数必须是静态方法。我既不是安全专家也不是缓存专家,所以我想知道是否有其他人已经编写了此代码。 - danludwig

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接