ASP.NET Web API 中的 HTTP Cookie

本主题介绍如何在 Web API 中发送和接收 HTTP Cookie。

HTTP Cookie 的背景

本部分简要概述了如何在 HTTP 级别实现 Cookie。 有关详细信息,请参阅 RFC 6265

Cookie 是服务器在 HTTP 响应中发送的一段数据。 客户端 (选择性地) 存储 Cookie,并在后续请求中返回它。 这允许客户端和服务器共享状态。 若要设置 Cookie,服务器在响应中包含 Set-Cookie 标头。 Cookie 的格式是具有可选属性的名称/值对。 例如:

Set-Cookie: session-id=1234567

下面是一个具有属性的示例:

Set-Cookie: session-id=1234567; max-age=86400; domain=example.com; path=/;

若要将 Cookie 返回到服务器,客户端在后续请求中包括 Cookie 标头。

Cookie: session-id=1234567

将 Cookie 返回到服务器的过程示意图,在此期间,客户端在后续请求中包含 Cookie 标头。

HTTP 响应可以包含多个Set-Cookie标头。

Set-Cookie: session-token=abcdef;
Set-Cookie: session-id=1234567;

客户端使用单个 Cookie 标头返回多个 Cookie。

Cookie: session-id=1234567; session-token=abcdef;

cookie 的范围和持续时间由 Set-Cookie 标头中的以下属性控制:

  • :告知客户端哪个域应接收 Cookie。 例如,如果域为“example.com”,则客户端会将 cookie 返回到 example.com 的每个子域。 如果未指定,则域为源服务器。
  • 路径:将 Cookie 限制为域中的指定路径。 如果未指定,则使用请求 URI 的路径。
  • 过期:设置 Cookie 的到期日期。 客户端会在 Cookie 过期时将其删除。
  • 最大期限:设置 Cookie 的最大期限。 客户端在 Cookie 达到最大期限时将其删除。

如果同时 Expires 设置了 和 Max-AgeMax-Age 则 优先。 如果两者均未设置,客户端将在当前会话结束时删除 Cookie。 (“会话”的确切含义由 user-agent.)

但请注意,客户端可能会忽略 Cookie。 例如,用户可能会出于隐私原因禁用 Cookie。 客户端可以在 Cookie 过期之前删除 Cookie,或限制存储的 Cookie 数。 出于隐私原因,客户端通常会拒绝域与源服务器不匹配的“第三方”Cookie。 简言之,服务器不应依赖于取回它设置的 Cookie。

Web API 中的 Cookie

若要将 Cookie 添加到 HTTP 响应,请创建表示 Cookie 的 CookieHeaderValue 实例。 然后调用 AddCookies 扩展方法,该方法在 System.Net.Http 中定义。HttpResponseHeadersExtensions 类,用于添加 Cookie。

例如,以下代码在控制器操作中添加 Cookie:

public HttpResponseMessage Get()
{
    var resp = new HttpResponseMessage();

    var cookie = new CookieHeaderValue("session-id", "12345");
    cookie.Expires = DateTimeOffset.Now.AddDays(1);
    cookie.Domain = Request.RequestUri.Host;
    cookie.Path = "/";

    resp.Headers.AddCookies(new CookieHeaderValue[] { cookie });
    return resp;
}

请注意, AddCookies 采用 CookieHeaderValue 实例数组。

若要从客户端请求中提取 Cookie,请调用 GetCookies 方法:

string sessionId = "";

CookieHeaderValue cookie = Request.Headers.GetCookies("session-id").FirstOrDefault();
if (cookie != null)
{
    sessionId = cookie["session-id"].Value;
}

CookieHeaderValue 包含 CookieState 实例的集合。 每个 CookieState 表示一个 Cookie。 使用索引器方法按名称获取 CookieState ,如下所示。

许多浏览器会限制它们将存储的 Cookie 数量,包括总数和每个域的数量。 因此,将结构化数据放入单个 Cookie 中很有用,而不是设置多个 Cookie。

注意

RFC 6265 不定义 Cookie 数据的结构。

使用 CookieHeaderValue 类,可以传递 Cookie 数据的名称/值对列表。 这些名称/值对在 Set-Cookie 标头中编码为 URL 编码的表单数据:

var resp = new HttpResponseMessage();

var nv = new NameValueCollection();
nv["sid"] = "12345";
nv["token"] = "abcdef";
nv["theme"] = "dark blue";
var cookie = new CookieHeaderValue("session", nv); 

resp.Headers.AddCookies(new CookieHeaderValue[] { cookie });

前面的代码生成以下Set-Cookie标头:

Set-Cookie: session=sid=12345&token=abcdef&theme=dark+blue;

CookieState 类提供索引器方法,用于从请求消息中的 Cookie 读取子值:

string sessionId = "";
string sessionToken = "";
string theme = "";

CookieHeaderValue cookie = Request.Headers.GetCookies("session").FirstOrDefault();
if (cookie != null)
{
    CookieState cookieState = cookie["session"];

    sessionId = cookieState["sid"];
    sessionToken = cookieState["token"];
    theme = cookieState["theme"];
}

示例:在消息处理程序中设置和检索 Cookie

前面的示例演示了如何在 Web API 控制器中使用 Cookie。 另一个选项是使用 消息处理程序。 与控制器相比,在管道中调用消息处理程序的时间要早。 消息处理程序可以在请求到达控制器之前从请求中读取 Cookie,或在控制器生成响应后将 Cookie 添加到响应中。

在消息处理程序中设置和接收 Cookie 的过程示意图。说明如何在管道中比控制器更早地调用消息处理程序。

以下代码演示用于创建会话 ID 的消息处理程序。 会话 ID 存储在 Cookie 中。 处理程序检查会话 Cookie 的请求。 如果请求不包含 Cookie,则处理程序将生成新的会话 ID。 在任一情况下,处理程序将会话 ID 存储在 HttpRequestMessage.Properties 属性包中。 它还会将会话 Cookie 添加到 HTTP 响应。

此实现不会验证来自客户端的会话 ID 是否实际由服务器颁发。 不要将其用作身份验证的一种形式! 此示例的要点是显示 HTTP Cookie 管理。

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;

public class SessionIdHandler : DelegatingHandler
{
    public static string SessionIdToken = "session-id";

    async protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        string sessionId;

        // Try to get the session ID from the request; otherwise create a new ID.
        var cookie = request.Headers.GetCookies(SessionIdToken).FirstOrDefault();
        if (cookie == null)
        {
            sessionId = Guid.NewGuid().ToString();
        }
        else 
        {
            sessionId = cookie[SessionIdToken].Value;
            try
            {
                Guid guid = Guid.Parse(sessionId);
            }
            catch (FormatException)
            {
                // Bad session ID. Create a new one.
                sessionId = Guid.NewGuid().ToString();
            }
        }

        // Store the session ID in the request property bag.
        request.Properties[SessionIdToken] = sessionId;

        // Continue processing the HTTP request.
        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

        // Set the session ID as a cookie in the response message.
        response.Headers.AddCookies(new CookieHeaderValue[] {
            new CookieHeaderValue(SessionIdToken, sessionId) 
        });

        return response;
    }
}

控制器可以从 HttpRequestMessage.Properties 属性包获取会话 ID。

public HttpResponseMessage Get()
{
    string sessionId = Request.Properties[SessionIdHandler.SessionIdToken] as string;

    return new HttpResponseMessage()
    {
        Content = new StringContent("Your session ID = " + sessionId)
    };
}