如何在ASP.NET MVC中关闭已认证用户的输出缓存?

16

我有一个ASP.NET MVC应用程序,我需要缓存一些页面,但只有未经身份验证的用户才能访问。

我尝试使用VaryByCustom="user"并提供以下GetVaryByCustomString实现:

public override string GetVaryByCustomString(HttpContext context, string custom)
{
  if (custom == "user")
  {
      if (context.User.Identity.IsAuthenticated)
      {
        return context.User.Identity.Name;
      }
      else
      {
        return "";
      }
  }  

  return base.GetVaryByCustomString(context, custom);
}

然而这并不完全符合我的需求,因为页面仍然被缓存了。唯一的区别是现在每个用户都有单独的缓存。

一种可能的解决方案是在用户经过身份验证时每次返回Guid.NewGuid(),但我认为这样会浪费大量资源。

所以,您有任何对我有用的提示吗?

3个回答

32

这是我所做的:

public class NonAuthenticatedOnlyCacheAttribute : OutputCacheAttribute
{
    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
      var httpContext = filterContext.HttpContext;

      if (httpContext.User.Identity.IsAuthenticated)
      {
        // it's crucial not to cache Authenticated content
        Location = OutputCacheLocation.None;
      }

      // this smells a little but it works
      httpContext.Response.Cache.AddValidationCallback(IgnoreAuthenticated, null);

      base.OnResultExecuting(filterContext);
    }

    // This method is called each time when cached page is going to be
    // served and ensures that cache is ignored for authenticated users.
    private void IgnoreAuthenticated(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
      if (context.User.Identity.IsAuthenticated)            
        validationStatus = HttpValidationStatus.IgnoreThisRequest;          
      else          
        validationStatus = HttpValidationStatus.Valid;          
    }
}

非常感谢Craig Stuntz,他指引了我正确的方向,而我不知情地将他的答案点了下去。


有趣 - 自从这篇文章(一年前)以来,您遇到过任何问题吗?谢谢。 - UpTheCreek
@UpTheCreek: 在我们的产品中,我们使用稍微复杂一些的这段代码版本。毫无疑问,根据我的经验,它有效果,但是我不能完全保证。 - Jakub Šturc
2
它对于页面非常有效,但不幸的是对于部分视图无效 - Cheburek
3
这样做行不通。一旦这个逻辑被执行并通过身份验证,所有后续的请求都将无法进行缓存。请记住,该属性是一个单实例,在整个HttpApplication实例的生命周期内存在于AppDomain中。要每个请求基于FilterAttribute更改缓存行为,您必须重新实现base.OnResultExecuting中的逻辑,以提供一个单独的实例给Page.InitOutputCache。请查看源代码。 - G-Wiz
1
gWiz是正确的 - 一旦您在IsAuthenticated检查中设置了位置,所有后续请求都不会被缓存。为了解决这个问题,请像这样缓存原始位置:https://dev59.com/aHI95IYBdhLWcg3w3yJU#9694955,然后将其用于未经授权的请求。 - Chris Hynes

12

属性通常会被缓存,因此您需要存储原始位置。如果您登录访问页面,则会将位置设置为无,然后当您以匿名方式访问时,它仍然为空。

public class AuthenticatedOnServerCacheAttribute : OutputCacheAttribute
{
    private OutputCacheLocation? originalLocation;

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        var httpContext = filterContext.HttpContext;

        if (httpContext.User.Identity.IsAuthenticated)
        {
            originalLocation = originalLocation ?? Location;
            Location = OutputCacheLocation.None;
        }
        else
        {
            Location = originalLocation ?? Location;
        }

        base.OnResultExecuting(filterContext);
    }
}

..但是你仍然需要接受的答案中的“IgnoreAuthenticated”部分。 - Alex from Jitbit

2
接受的答案是正确的,但它在这种部分视图缓存的方式上不起作用。我将两种方法结合起来使用:使用GetVaryByCustomString并将Duration设置为最小值-用于局部视图,以及使用AddValidationCallback方法进行页面设置。实际上,只使用第一种方法也是可行的,但第二种方法看起来不那么昂贵-它不会每次都调用OnResultExecuting,而只调用注册的处理程序。
所以这是自定义缓存属性类。
public class CacheAttribute : OutputCacheAttribute
{   

    public CacheAttribute()
    {
      Duration = 300;  /*default cache time*/
    }

    private bool _partialView;

    /// <summary>
    /// Set true if Partial view is cached
    /// </summary>
    public bool PartialView
    {
      get { return _partialView; }
      set
      {
        _partialView = value;
        if ( _partialView ) {
          VaryByCustom = "Auth";
        }
      }
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        if ( PartialView ) OnCachePartialEnabled( filterContext );
        else OnCacheEnabled(filterContext);

        base.OnResultExecuting( filterContext );     
    }

    private OutputCacheLocation? originalLocation;
    private int? _prevDuration;
    protected void OnCachePartialEnabled(ResultExecutingContext filterContext)
    {
      var httpContext = filterContext.HttpContext;

      if ( !_prevDuration.HasValue) _prevDuration = Duration;
      Duration = httpContext.User.Identity.IsAuthenticated ? 1 : _prevDuration.Value;
    }

    protected void OnCacheEnabled(ResultExecutingContext filterContext)
    {
      var httpContext = filterContext.HttpContext;

      if ( httpContext.User.Identity.IsAuthenticated ) {
        // it's crucial not to cache Authenticated content
        originalLocation = originalLocation ?? Location;
        Location = OutputCacheLocation.None;
      }
      else {
      Location = originalLocation ?? Location;
    }

      // this smells a little but it works
      httpContext.Response.Cache.AddValidationCallback( IgnoreAuthenticated, null );      
    }

    // This method is called each time when cached page is going to be
    // served and ensures that cache is ignored for authenticated users.
    private void IgnoreAuthenticated(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
      validationStatus = context.User.Identity.IsAuthenticated 
        ? HttpValidationStatus.IgnoreThisRequest 
        : HttpValidationStatus.Valid;
    }
}

在Global.asax.cs中重写GetVaryByCustomString方法。

public override string GetVaryByCustomString(HttpContext context, string custom)
{ 
    if ( custom == "Auth" ) {
      //do not cache when user is authenticated
      if ( context.User.Identity.IsAuthenticated ) {
        return base.GetVaryByCustomString( context, custom );
      }
      return "NotAuth";
    }     
    return base.GetVaryByCustomString( context, custom );
}

使用方法如下:

[Cache]
public virtual ActionResult Index()
{
    return PartialView();
}

[ChildActionOnly, Cache(PartialView=true)]
public virtual ActionResult IndexPartial()
{
    return PartialView();
}

更新:我在这里也添加了Fujiy的修复方法。


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