65.9K
CodeProject 正在变化。 阅读更多。
Home

关于 Cookie 的处理和管理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (58投票s)

2002 年 10 月 27 日

13分钟阅读

viewsIcon

624106

downloadIcon

2069

您一直想了解的关于 ASP.NET Cookie 的一切,但又不敢问。

只需将文件拖放到任何 Web 应用程序中,将 .aspx 文件包含在项目中,并将其设置为启动页。

引言

那么,cookie 究竟是什么?根据 Websters Online 的说法,cookie 可以是以下任何一种:

  1. 一种扁平或略微隆起的小饼干
  2. a:一个有魅力的女人 <一个风骚的法国女人,经常出没于……该殖民地的唯一夜生活场所——新闻周刊> b:人,家伙 <一个坚强的家伙>
  3. cookie:存储在万维网用户计算机上的一个小型文件或文件的一部分,由网站服务器创建并随后读取,其中包含个人信息(如用户标识代码、自定义首选项或访问过的页面记录)。

尽管其他定义可能很诱人,但我们在这里关注的是第三个。Cookie 是一种信息小包,作为 HTTP 响应的一部分发送,存储在客户端机器上,随后作为任何 HTTP 请求的一部分发送到原始网站。在 ASP.NET terms 中,页面接收一个 CookieCollection 作为 HttpRequest 对象的属性(通常是 Request.Cookies),并返回一个 CookieCollection 作为 HttpResponse 对象的属性(通常是 Response.Cookies)的更新。

Cookie 通常用于各种级别的状态管理:可能是为了让用户保持登录到某个站点,或者跟踪用户上次访问某个站点(或站点区域)的时间。

我最近使用 cookie 来跟踪一个锦标赛报名过程,在这个过程中,一名队长可能一次报名多达 10 名队员。我非常确定,在某个时候,用户的浏览器可能会崩溃,或者客户端或服务器机器可能会崩溃,或者用户可能会点击菜单中的另一个链接。我不想让他们从头开始,所以我将一个会话 ID 存储在 cookie 和数据库中的每个报名记录中。下次用户再次访问报名页面时,可以轻松检索到这个会话 ID,我就可以从数据库中提取所有数据,为用户节省大量时间。

Cookie 是 Web 设计中的一个非常强大的工具,但在 ASP.NET 中,它也可能导致许多问题,特别是对于 ASP 用户(ASP 对 cookie 的处理方式略有不同)。这里的任何东西都不是火箭科学,但只有当你理解了幕后发生的事情,它们才是简单的。

Cookie 过期

关于 cookie,你需要了解的第一件事是:cookie 带有过期日期。你需要了解的第二件事是:过期日期是导致大多数与 cookie 相关错误的根本原因。

每次设置 cookie 的 Value 时,请记住也要设置 Expires 日期。如果你不这样做,你会很快发现自己丢失了 Cookie,因为它们在客户端机器上更新时或在浏览器关闭时会立即过期。

当 cookie 过期时,客户端不再将其发送到服务器,因此你需要确保 cookie 的 Expires 属性始终是未来的。如果你只设置了一个 cookie 的值,那么它将创建一个 Expires 设置为 DateTime.MinValue(0001 年 1 月 1 日 00:00)的 cookie。

你可以使用任何 DateTime 值来设置 cookie 的 Expires 属性(这比 ASP 令人欣慰)。例如,如果你希望一个 Cookie 在用户一段时间未访问你网站的该部分后过期,你将设置 Expires = DateTime.Now.AddDays(7)

如果你希望 cookie 永久存在,那么诱惑是使用 DateTime.MaxValue,就像我在本文的早期版本中那样。然而,这里有一个简单的陷阱。

DateTime.MaxValue 精确到 9999 年 12 月 31 日 25:59:59.9999999。但是 Netscape,即使是 7 版本,也无法处理这个值,会立即使 cookie 过期。令人惊讶的是,也有些令人沮丧的是,调查显示 Netscape 7 可以处理 9999 年 11 月 10 日 21:47:44,但无法处理更高的秒(老实说,我没有测试到任何小数秒,我真的不感兴趣)。

因此,如果你像我一样,订阅“只要在最新版本上功能正常,在 Netscape 上看起来不美观也没关系”的观点,那么总是使用早于该日期的日期。一个普遍接受的“永久”cookie 过期日期是 DateTime.Now.AddYears(30),即 30 年后的日期。如果有人这么长时间没有访问你的网站,你真的会在乎他们上次访问时的状态吗?

删除过期 Cookie

如果你想在客户端机器上删除 cookie,**不要**使用显而易见的 Response.Cookies.Remove("MyCookie"),这只会告诉 cookie 不要覆盖客户端的 cookie(下面有更详细的解释),而是将 cookie 的 Expires 属性设置为任何早于当前时间的日期。这将告诉客户端用一个过期的 cookie 覆盖当前的 cookie,客户端将永远不会将其发送回服务器。

同样,诱惑是使用 DateTime.MinValue(0001 年 1 月 1 日 00:00:00)来删除 cookie;同样,这会是一个错误。这次,Netscape 7 会按预期工作,但 Internet Explorer 6 会认为这是一个特殊情况。如果 Internet Explorer 收到一个它认为是“空白”过期日期的 cookie,它会保留该 cookie 直到浏览器关闭,然后使其过期。

当然,如果行为在所有浏览器中都一致,这可能会很有用。不幸的是,情况并非如此,试图使用 Internet Explorer 的功能会在 Netscape 中查看页面时导致页面失败。

另一个容易陷入的陷阱:理论上,你可以使用 DateTime.Now 来立即使 cookie 过期,但这有一些危险。

如果服务器机器的时间与客户端机器的时间不同步,客户端可能会认为给定的时间在(尽管是近期)未来。这可能导致在上传到实时服务器时出现一个在本地测试时不明显的 bug。更糟糕的是,这可能会造成一种情况,即当你查看 Web 应用程序时它运行正常,但当其他用户从他们的机器访问时却不行。

这两种情况都非常难以调试。

最安全(也是最对称)的删除 cookie 的方法是使用 DateTime.Now.AddYears(-30) 作为过期日期。

传入的 Cookie

当接收到一个页面时,它在 Request 中包含一个 CookieCollection,列出了客户端机器上该命名空间下的所有 cookie。任何不存在于 Request 中的 cookie,如果你尝试访问它们,将为 null(因此,除非你确定它存在,否则要小心查找 Value 属性)。

另一方面,对于 Response,你的代码开始时没有 cookie,它们在你需要时才被创建。当服务器发送 Response 时,客户端机器只调整 Response.Cookies 集合中存在的 Cookie;其他 cookie 不受影响。

似乎命运的奇特转折是,传入的(Request)cookie 带有 DateTime.MinValueExpires 日期,无论客户端系统上的 cookie 附加了什么日期。

这实际上很容易解释——正如许多 Web 开发人员所知,一旦 cookie 被写入客户端机器,几乎不可能获取其过期日期(尝试在 JavaScript 中执行)。它肯定不会作为请求的一部分发送。但微软希望 Response 和 Request cookie 属于同一类(HttpCookie)。由于 DateTime 是一个值对象,而不是引用对象,它不能为 null,所以它必须有一个值。最好的任意值是 DateTime.MinValue

尽管这可以理解,但如果我们不小心,这又是我们可能出错的地方。如果我们想直接将一个 Request cookie 复制到 Response(我们稍后会看到这是一个有用的工具),那么我们就需要创建一个新的过期日期,即使我们可以安全地假设旧日期是可以的。

神秘消失的 Cookie 案

如果你尝试访问 Response.Cookies 集合中不存在的 cookie,它将被创建,其 Value 为空字符串,Expires 日期为 0001 年 1 月 1 日 00:00。奇怪的是,如果 Request.Cookies 集合中不存在匹配的 cookie,它也会创建一个匹配的 cookie。

所以,如果你查看 Response 中的 cookie,那么你就是在间接用一个空的 cookie 覆盖客户端机器上的 cookie,该 cookie 会在浏览器关闭时过期(或者在 Netscape 中立即过期)。

演示将有助于说明这一点。

考虑一个包含一个显示 cookie 的标签的单个网页。三个命令按钮都将页面重定向到自身,第一个设置 cookie,第二个清除 cookie,第三个什么也不做(见图)。

为了清晰起见,标签周围还有一个凹槽式边框,并且标签默认情况下会填充连字符(“-”),所以我们可以确切地看到正在发生什么。即,我们不想将空字符串与根本没有填充标签混淆。


<asp:label id="myRequestCookie"
  style="Z-INDEX: 101; LEFT: 26px; POSITION: absolute; TOP: 22px"
  runat="server" Width="220px" BorderStyle="Groove">
  -----------------------------------</asp:label>
<asp:button id="btnCookies.Set"
  style="Z-INDEX: 102; LEFT: 26px; POSITION: absolute; TOP: 56px"
  runat="server" Width="220px" Text="Set Cookie"></asp:button>
<asp:button id="btnClearCookie"
  style="Z-INDEX: 103; LEFT: 26px; POSITION: absolute; TOP: 84px"
  runat="server" Width="220px" Text="Clear Cookie"></asp:button>
<asp:Button id="btnDoNothing"
  style="Z-INDEX: 104; LEFT: 26px; POSITION: absolute; TOP: 112px"
  runat="server" Width="220px" Text="Do Nothing"></asp:Button>


private void Page_Load(object sender, System.EventArgs e)
{
    // Display the Request cookie on the page
    if (Request.Cookies["TestCookie"] == null)
        myRequestCookie.Text = "No cookie found";
    else
        myRequestCookie.Text = Request.Cookies["TestCookie"].Value;
}

private void btnCookies.Set_Click(object sender, System.EventArgs e)
{
    // Set up a cookie and redirect to this page to pick it up for display
    Response.Cookies["TestCookie"].Value = "Cookie is set";
    Response.Cookies["TestCookie"].Expires = DateTime.Now.AddYears(30);
    Response.Redirect("Example5.1.aspx");
}

private void btnClearCookie_Click(object sender, System.EventArgs e)
{
    // Expire the cookie and redirect to this page to display a message
    Response.Cookies["TestCookie"].Expires = DateTime.Now.AddYears(-30);
    Response.Redirect("Example5.1.aspx");
}

private void btnDoNothing_Click(object sender, System.EventArgs e)
{
    // Do absolutely nothing except redirect to simulate moving to another page
    Response.Redirect("Example5.1.aspx");
}

你期望这个页面始终显示最新的 cookie 状态:设置或清空。当你按下“不做任何事”按钮时,你期望状态保持不变。

好吧,你猜怎么着。这正是它所做的。然而……

如果你现在在 Page_Load 事件处理程序的第一个行上放置一个断点,并在 Response.Cookies["TestCookie"].Value 上添加一个调试器监视,那么奇怪的事情就开始发生。

当你设置 cookie 时,它似乎被设置了;当你清除它或什么都不做时(无论当前状态如何),你会得到一个空字符串(见图)。

这是因为调试器仅仅通过查看 cookie 是否存在就创建了一个空的 cookie;这个新的空白 cookie 会在浏览器打开期间一直存在,然后过期。这个空白 cookie 现在会显示在你的标签中。

当你点击“设置 Cookie”按钮时,第一个空白的响应 cookie 被一个有效的(且未过期的)cookie 覆盖,因此当它返回时,有一个请求 cookie,当你不中断时不会被自动覆盖。**但是**,当 Response 返回时,标签被正确填充,它还有一个无效的空白 cookie 会立即过期,所以即使那时页面也没有正确工作,尽管它一开始看起来是正确的。

这可能**极其**危险,如果你正在调试一个设置 Cookie 的页面,然后在调试另一个不设置 Cookie 的页面时,让监视器保持可见。

实际问题

既然我们已经确切地了解了正在发生的事情,我们就可以预测潜在的问题并轻松修复它们。

最常见的困境可能是这样的情况:你想在代码的一个部分有条件地更新一个 cookie,然后在稍后获取“当前”值。请仔细考虑以下代码:

private void Page_Load(object sender, System.EventArgs e)
{
    // ...


    // Set cookie only under given situation

    if (myCondition)
    {
        Response.Cookies["MyCookie"].Value = myValue;
        Response.Cookies["MyCookie"].Expires = DateTime.Now.AddYears(30);
    }

    // ...

}

private void MyButton_OnClick(object sender, System.EventArgs e)
{
    // ...


    // If there's an updated cookie then get it, otherwise get the old one

    if (Response.Cookies["MyCookie"] == null)
        currentCookieValue = Request.Cookies["MyCookie"].Value;
    else
        currentCookieValue = Response.Cookies["MyCookie"].Value;

    // ...

}

正如你所猜到的,因为你刚才看到了潜在问题的残酷演示,这段代码将不会像开发者期望的那样工作。检查 cookie 是否为 null 的行为本身就会创建 cookie。第二个条件**永远**不会为真,并且 currentCookieValue 将被赋予一个空字符串,无论 cookie 是否是由于第一个条件而明确创建的。

这可能很难调试,因为两段代码可能不会靠得这么近。我们已经看到了在 Response cookie 上设置监视会发生什么,所以最好避免这种方法,而且为了让开发者完全感到困惑,cookie 会立即过期,所以它不会出现在下一个请求中。

预防胜于治疗

这里的可能解决方案不计其数,最明显的是将第二个条件更改为移除在创建它的地方的无效 cookie。

private void MyButton_OnClick(object sender, System.EventArgs e)
{
    // ...


    // If there's an updated cookie then get it, otherwise get the old one

    if (Response.Cookies["MyCookie"].Value == ""
        && Response.Cookies["MyCookie"].Expires == DateTime.MinValue)
    {
        currentCookieValue = Request.Cookies["MyCookie"].Value;
        Response.Cookies.Remove("MyCookie");
    }
    else
    {
        currentCookieValue = Response.Cookies["MyCookie"].Value;
    }

    // ...

}

当然,如果你必须多次复制此代码,很快这个解决方案就会变得笨拙。

一个更干净的解决方案是确保每个可能更新 cookie 的页面都从 Request.Cookies 集合复制到 Response.Cookies 集合;所有处理都将相对于响应 cookie 进行。因此,上面的例子变为:

private void Page_Load(object sender, System.EventArgs e)
{
    // Ensure preservation of cookie

    if (Request.Cookies["MyCookie"] != null)
        Response.Cookies.Set(Request.Cookies["MyCookie"]);
    else
        Response.Cookies.Set(new HttpCookie("MyCookie", "DefaultValue"));

    // The Request Cookie doesn't include expiry date, so you need to add one

    // in either case

    Response.Cookies["MyCookie"].Expires = DateTime.Now.AddYears(30);

    // ...


    // Change cookie value only under given situation

    if (myCondition)
        Response.Cookies["MyCookie"].Value = myValue;

    // ...

}

private void MyButton_OnClick(object sender, System.EventArgs e)
{
    // ...


    // Response.Cookies will always hold the current value

    currentCookieValue = Response.Cookies["MyCookie"].Value;

    // ...

}

这唯一的缺点是它会产生过多的带宽使用;你可能会发送回一个包含客户端发送的相同详细信息的 cookie。如果它是一个小的单个 cookie,那么这不是一个严重的问题。你发送回 cookie 集合中的任何内容,与页面本身相比,几乎都是微不足道的。

如果你确实认为带宽会成为一个问题,或者只是想超级高效,那么最好的方法是在发送 Response 之前删除那些不会更新原始 cookie 的 cookie。

protected override void OnPreRender(System.EventArgs e)
{
    // Remember that if the request cookie was null, it

    // is created by looking at the response cookie

    if (Response.Cookies["TestCookie"].Value == Request.Cookies["TestCookie"].Value)
        Response.Cookies.Remove("TestCookie");

    base.OnPreRender(e);
}

尝试将此技术应用于“丢失的 cookie”示例,记住你不应依赖 null 值,而应使用默认值(可能是空)字符串;你会发现你现在可以安全地进行调试。这是本文顶部提供的可下载示例。

完成后,请务必删除监视器。如果你在编辑一个可能根本不查看特定 cookie 的页面时监视器仍然处于活动状态,你可能会导致 cookie 被清空。

结论

与大多数 .NET 事物一样,潜在的问题既严重又很少被记录下来,但一旦你实现了它们几次,解决方案就很简单。

三个简单的规则

  • 每当你设置 Value 时,永远不要忘记设置 Expires 日期。
  • 无论何时访问 Response.Cookies 集合,都要确保你不是无意中创建了一个空白和/或过期的 cookie。
  • 避免使用 DateTime.NowDateTime.MinValueDateTime.MaxValue
记住这些,你不会走错路。

记住:善待你的 cookie,它们会成为你的好朋友;粗暴对待它们,它们会咬你。

修订历史

01-Jan-03

  • 修改了“Cookie 过期”部分,以引用 DateTime.MaxValue Netscape 过期异常。
  • 修改了“Cookie 过期”部分,以引用 DateTime.MinValue Internet Explorer 异常。
  • 修改了“Cookie 过期”部分,以引用 DateTime.Now 时间同步异常。
  • 修改了代码和示例,以避免上述陷阱。
  • 根据上述陷阱,在“结论”中添加了第三条简单规则。
  • 添加了“处理过期 Cookie”部分,将“Cookie 过期”分解得更细致。
  • 修正了“预防胜于治疗”中第一个代码示例中的几个错误。
  • 将“神秘消失的 Cookie”的第一部分移到了新章节“传入的 Cookie”的开头,以便在解释另一个问题之前介绍这个概念。
  • 修改代码以使用 Response.Cookies.Set 而不是 Response.Cookies.Add,因为这似乎是一个好习惯,尽管我还不完全确定其影响。
© . All rights reserved.