ASP.NET输出缓存和Cookies

9

有人知道为什么我的页面上有cookie时,输出缓存不起作用吗?

示例页面

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="ct.aspx.vb" Inherits="ct" %>
<%@ OutputCache Duration="600" Location="Server" VaryByParam="none" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
      <h1>Cache test</h1>
      <p id="rndout" runat="server"></p>
    </div>
    </form>
</body>
</html>

示例代码如下:

Partial Class ct
    Inherits System.Web.UI.Page

    Protected Sub Page_Load(sender As Object, e As System.EventArgs) Handles Me.Load
        Dim rc As New Random()
        Dim rn As Integer
        rn = rc.Next()
        rndout.InnerHtml = rn.ToString

        Response.Cookies("sym")("hello") = "world"
        Response.Cookies("sym").Expires = DateTime.Now.AddDays(370)
        Response.Cookies("sym").Domain = Application.Get("cookieurl")

    End Sub
End Class

当部署到IIS 6或7时,这不会缓存,但是如果我注释掉3个Response.Cookies行,则会缓存。

在VS中运行时,两种方式都可以正常工作。

是否有一些在IIS / web.config等中的设置可以允许输出缓存,同时我设置了response.cookies。我理解cookie内容也将被缓存,因为它只是被缓存的http响应的一部分。


1
我发现同样的事情是正确的,但是没有遇到任何官方文档明确说明它不起作用。 - JNappi
@Allov,抱歉耽搁了 - 我还没有找到解决方案。除了删除 cookie 或者如果我需要一个 cookie,我可以在页面上添加一个脚本标签或 0x0 图像来设置 cookie。 - Symeon Breen
有些晚了,但这个人还是想出了一个解决方法。目前还没有消息表明它有多大的风险。https://dev59.com/Qonda4cB1Zd3GeqPFukE#29771803 - Lavamantis
6个回答

6

在对这个问题进行了一定的研究后,我理解并解决了这个问题。

输出缓存与Cookie不兼容的原因

输出缓存不能缓存带有Cookie的响应的原因是Cookie可能是特定于用户的(例如,身份验证、分析跟踪等)。如果一个或多个具有属性HttpCookie.Shareable = false的Cookie,则输出缓存将认为该响应无法缓存。

包括缓存响应的Cookie

这里就会变得棘手。输出缓存将响应头和内容一起缓存,并且不提供任何钩子来修改它们在发送回用户之前。然而,我编写了以下自定义输出缓存提供程序,以便在将其发送回用户之前修改缓存响应的标头(需要Fasterflect nuget包):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
using System.Web;
using System.Web.Caching;
using Fasterflect;

namespace CustomOutputCache
{
    /// <summary>
    /// An output cache provider that has ability to modify the http header collection before a cached response is served back to the user.
    /// </summary>
    public class HeaderModOutputCacheProvider : OutputCacheProvider
    {
        private static readonly Type OutputCacheEntryType, HttpCachePolicySettingsType;
        private static readonly Type[] ParameterTypes;

        public static event EventHandler<CachedRequestEventArgs> RequestServedFromCache;

        static HeaderModOutputCacheProvider()
        {
            var systemWeb = typeof(HttpContext).Assembly;
            OutputCacheEntryType = systemWeb.GetType("System.Web.Caching.OutputCacheEntry");
            HttpCachePolicySettingsType = systemWeb.GetType("System.Web.HttpCachePolicySettings");
            ParameterTypes = new[]{
                typeof(Guid),
                HttpCachePolicySettingsType,
                typeof(string),
                typeof(string) ,
                typeof(string[]),
                typeof(int),
                typeof(string),
                typeof(List<HeaderElement>),
                typeof(List<ResponseElement>)
            };
        }

        private readonly ObjectCache _objectCache;

        public HeaderModOutputCacheProvider()
        {
            _objectCache = new MemoryCache("output-cache");
        }

        #region OutputCacheProvider implementation

        public override object Get(string key)
        {
            var cachedValue = _objectCache.Get(key);

            if (cachedValue == null)
                return null;

            if (cachedValue.GetType() != OutputCacheEntryType)
                return cachedValue;

            var cloned = CloneOutputCacheEntry(cachedValue);

            if (RequestServedFromCache != null)
            {
                var args = new CachedRequestEventArgs(cloned.HeaderElements);
                RequestServedFromCache(this, args);
            }

            return cloned;
        }

        public override object Add(string key, object entry, DateTime utcExpiry)
        {
            _objectCache.Set(key, entry, new CacheItemPolicy { AbsoluteExpiration = utcExpiry });
            return entry;
        }

        public override void Set(string key, object entry, DateTime utcExpiry)
        {
            _objectCache.Set(key, entry, new CacheItemPolicy { AbsoluteExpiration = utcExpiry });
        }

        public override void Remove(string key)
        {
            _objectCache.Remove(key);
        }

        #endregion

        private IOutputCacheEntry CloneOutputCacheEntry(object toClone)
        {
            var parameterValues = new[]
            {
                toClone.GetFieldValue("_cachedVaryId", Flags.InstancePrivate),
                toClone.GetFieldValue("_settings", Flags.InstancePrivate),
                toClone.GetFieldValue("_kernelCacheUrl", Flags.InstancePrivate),
                toClone.GetFieldValue("_dependenciesKey", Flags.InstancePrivate),
                toClone.GetFieldValue("_dependencies", Flags.InstancePrivate),
                toClone.GetFieldValue("_statusCode", Flags.InstancePrivate),
                toClone.GetFieldValue("_statusDescription", Flags.InstancePrivate),
                CloneHeaders((List<HeaderElement>)toClone.GetFieldValue("_headerElements", Flags.InstancePrivate)),
                toClone.GetFieldValue("_responseElements", Flags.InstancePrivate)
            };

            return (IOutputCacheEntry)OutputCacheEntryType.CreateInstance(
                parameterTypes: ParameterTypes,
                parameters: parameterValues
            );
        }

        private List<HeaderElement> CloneHeaders(List<HeaderElement> toClone)
        {
            return new List<HeaderElement>(toClone);
        }
    }

    public class CachedRequestEventArgs : EventArgs
    {
        public CachedRequestEventArgs(List<HeaderElement> headers)
        {
            Headers = headers;
        }
        public List<HeaderElement> Headers { get; private set; }

        public void AddCookies(HttpCookieCollection cookies)
        {
            foreach (var cookie in cookies.AllKeys.Select(c => cookies[c]))
            {
                //more reflection unpleasantness :(
                var header = cookie.CallMethod("GetSetCookieHeader", Flags.InstanceAnyVisibility, HttpContext.Current);
                Headers.Add(new HeaderElement((string)header.GetPropertyValue("Name"), (string)header.GetPropertyValue("Value")));
            }
        }
    }
}

你需要这样连接它:
<system.web>
  <caching>
      <outputCache defaultProvider="HeaderModOutputCacheProvider">
        <providers>
          <add name="HeaderModOutputCacheProvider" type="CustomOutputCache.HeaderModOutputCacheProvider"/>
        </providers>
      </outputCache>
    </caching>
  </system.web>

你可以像这样使用它来插入cookies:

HeaderModOutputCacheProvider.RequestServedFromCache += RequestServedFromCache;

HeaderModOutputCacheProvider.RequestServedFromCache += (sender, e) =>
{
    e.AddCookies(new HttpCookieCollection
    {
        new HttpCookie("key", "value")
    });
};

4
你尝试在服务器端缓存此页面,同时在客户端设置cookie - 这两者不能同时进行。
为什么会这样呢:当你在服务器端缓存一个页面时,当缓存版本被提供给客户端时,后台代码不会运行。这就是服务器端缓存的目的。不运行任何东西,直接从缓存中提供。
也许你只需要在头部设置缓存,而不是在服务器端缓存整个页面。

1
我正在创建一个带有cookie的asp.net页面。我希望iis缓存此页面并且不运行代码后台。我正在使用标准的.net代码来实现这一点。然而,如果我以任何方式使用response.cookie,则outputcache指令会中断。这在.net中没有任何文档记录。事实上,有一篇文章说要记住,如果您缓存带有cookie的页面,则cookie也会被缓存。我非常清楚cookie是http头的一部分,因此将被缓存。我的问题是是否有iis/web.config等设置可以启用此功能。在cassini中运行时可以正常工作。 - Symeon Breen
@Symeon,你尝试做的不合逻辑(有漏洞)。你将一个cookie设置给一个用户,那么下一个没有设置cookie的用户怎么办?Cookie是在客户端设置的-你混淆了客户端缓存和服务器缓存。当cookie在客户端而不是服务器上时,它们也被保存在缓存中。 - Aristos
1
无论是否存在,访问该页面的人都将获得一个cookie。Cookie只是http头中的文本。我可以理解为什么会感到困惑,但我认为必须有一些配置,因为没有地方说明它们是互斥的,并且在cassini中运行良好。看看这个-http://support.microsoft.com/kb/917072听起来应该使用cookie进行缓存,因为他们提供了一个解决方法来停止它。 - Symeon Breen

3
不同版本的.NET框架导致了这个问题。基本上,某些版本将永远不会缓存设置有cookie的页面。
参阅此博客文章

欢迎来到 Stack Overflow!感谢您发布您的答案!请务必仔细阅读有关自我推广的FAQ。同时请注意,每次链接到您自己的网站/产品时都需要发布免责声明。 - Andrew Barber

1

请检查您是否正在运行.NET 2.0 SP1,并且是否应用了MS11-100(发布于2012年12月)。

我们遇到了类似的问题,最终联系了微软支持。他们确认MS11-100破坏了输出缓存,但声称这是设计上的问题(由于修补程序中解决的安全漏洞的性质),目前没有任何措施来恢复输出缓存功能。

一个简单的测试:如果您发现已安装该补丁程序,请卸载该补丁程序并重新启动。您应该会看到输出缓存开始工作。我不认为有人会建议将此作为生产解决方案,因为存在安全隐患,因此只能将其用作隔离问题的手段。

我们最终测试了一个更新的框架(您必须使用4.0版本;3.5只是2.0框架的扩展,而不是独立的框架),在解决所有编译错误后,输出缓存立即开始工作。

我们还致力于改变与Cookie的交互方式,以便我们可以保持在2.0框架上(毕竟,测试我们的Cookie处理程序类应该比测试整个应用程序更容易)。有许多障碍,最终产品充满了“黑客”技巧,所以这是不可行的。


0

我遇到了同样的问题,我测试了 Aristos 给出的情景,设置 Location="ServerAndClient" 即可解决。如果我只使用 Location="Server" 则无法解决该问题。


在您的情况下,只有客户端(HTTP响应头缓存)会起作用。如果您在响应中设置了cookie,则页面输出不会被服务器缓存。 - d4n3

-1
有一个解决方法在某些情况下可能有效: 如果Cookie不太依赖于页面代码,但可以通过一些独立的代码计算,则可以在Application_EndRequest中设置Cookie。 Application_EndRequest在OutputCache之后处理,因此缓存将不带Cookie存储,但然后在请求传递到客户端之前添加了设置Cookie头。

我尝试了这种方法,但收到了“无法在响应已发送后修改标头”的错误。 - WiseGuyEh

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