21

使用 Windows AzureMicrosoft.Web.DistributedCache.DistributedCacheOutputCacheProvider作为 MVC3 应用程序的 outputCache 提供程序。下面是相关的操作方法:

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

从 Web 浏览器加载此视图时出现以下异常:

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] 属性,视图会按预期缓存。这是否意味着我不能将 [OutputCache] 放在必须具有 [Authorize] 的操作方法上?或者,我是否需要使用对缓存使用静态验证回调方法的自定义实现来覆盖 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 身份访问示例缓存页面时,页面的输出被缓存,并且视图显示“此页面已缓存于 12/31/2011 11:06:12 AM (UTC)”。如果我随后注销并以 another@otherdomain.tld 身份登录并访问该页面,它会显示“此页面已缓存于 12/31/2011 11:06:38 AM (UTC)”。以 me@mydomain.tld 身份重新登录并重新访问该页面会导致缓存再次显示“此页面已缓存于 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 实例的用户和角色属性。但是,似乎没有一种简单的插入方法。我必须重写 OnAuthorization 以将这两个值放入 HttpContext(Items 集合?),然后重写 OnCacheAuthorization 以将它们取出。但这闻起来很脏。

如果我们小心地在 OutputCache 属性中使用 VaryByCustom = "User" 属性,我们是否可以重写 OnCacheAuthorization 以始终返回 HttpValidationStatus.Valid?当 action 方法没有 OutputCache 属性时,我们不需要担心这个回调会被调用,对吗?如果我们确实有一个没有 VaryByCustom = "User" 的 OutputCache 属性,那么很明显页面可以返回任何缓存版本,而不管哪个用户请求创建了缓存副本。这有多大风险?

4

3 回答 3

10

缓存发生在动作之前。您可能需要自定义授权机制来处理缓存方案。

查看我不久前发布的一个问题 - MVC Custom Authentication, Authorization, and Roles Implementation

我认为对您有帮助的部分是自定义授权属性,其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;
}
于 2012-01-06T19:45:43.547 回答
7

我回到了这个问题,经过一番修改后,我得出结论,在使用 Azure DistributedCache 时,您不能同时使用System.Web.Mvc.AuthorizeAttribute开箱即用的产品。主要原因是,正如原始问题中的错误消息所述,验证回调方法必须是静态的,才能与 Azure 的 DistributedCache 一起使用。MVC Authorize 属性中的缓存回调方法是一个实例方法。System.Web.Mvc.OutputCacheAttribute

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

授权

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,这是另一个实例方法(受保护的虚拟)。AuthorizeCore,在 OnAuthorization 期间也被调用,主要做 3 件事:

  1. 检查 HttpContextBase.User.Identity.IsAuthenticated == true

  2. 如果属性具有非空的用户字符串属性,则检查 HttpContextBase.User.Identity.Name 是否与逗号分隔值之一匹配。

  3. 如果属性具有非空的 Roles 字符串属性,请检查 HttpContextBase.User.IsInRole 是否有逗号分隔值之一。

授权核心

// 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 字段。

我的第一次尝试是object data使用CacheValidateHandler. 即使在引入静态方法之后,这仍然不起作用,并导致同样的异常。我希望对象数据会被序列化,然后在回调期间传回验证处理程序。显然情况并非如此,当您尝试这样做时,Azure 的 DistributedCache 仍将其视为非静态回调,从而导致相同的异常和消息。

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

我的第二次尝试是将值添加到HttpContext.Items集合中,因为 的实例HttpContext会自动传递给处理程序。这也不起作用。HttpContext传递给 的与属性上存在的CacheValidateHandler 实例不同。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));
}

然而...

即使似乎没有办法将 Users & Roles 属性值传递回缓存验证回调处理程序,但HttpContext传递给它的实际上确实具有正确的 User Principal。此外,我目前想要组合 [Authorize] 和 [OutputCache] 的任何操作都不会将用户或角色属性传递给 AuthorizeAttribute 构造函数。

因此,可以创建一个忽略这些属性的自定义 AuthenticateAttribute,并且只检查以确保User.Identity.IsAuthenticated == true. 如果您需要针对特定​​角色进行身份验证,您也可以这样做并与 OutputCache 结合使用......但是,您需要为每个(一组)角色设置一个不同的属性,以使缓存验证回调方法静态. 稍微完善一下后,我会回来发布代码。

于 2012-05-12T12:26:53.377 回答
2

你是正确的橄榄。缓存的工作原理是缓存 Action 的整个输出(包括所有属性),然后将结果返回给后续调用,而无需实际调用任何代码。

因此,您无法缓存和检查授权,因为通过缓存您不会调用任何代码(包括授权)。因此,任何被缓存的东西都必须是公开的。

于 2011-12-30T20:39:23.717 回答