# 五、状态与安全

在最后一章中，我们将了解 HTTP 的安全方面，包括如何识别用户，HTTP 身份验证如何工作，以及为什么有些场景需要 HTTPS(安全 HTTP)。在此过程中，我们还将学习如何使用 HTTP 管理状态。

## 无状态(但有状态)网络

HTTP 是一种无状态协议，这意味着每个请求-响应事务都独立于任何以前或将来的事务。HTTP 协议中没有任何内容要求服务器保留关于 HTTP 请求的信息。服务器需要做的就是为每个请求生成一个响应。每个请求都将携带服务器创建响应所需的所有信息。

HTTP 的无状态特性是网络成功的驱动因素之一。我们在上一章中看到的分层服务，如缓存，都成为可能(或至少更容易)，因为每条消息都包含处理消息所需的所有信息。代理服务器和 web 服务器可以检查、转换和缓存消息。没有缓存，网络就无法扩展以满足互联网的需求。

然而，我们在 HTTP 之上构建的大多数 web 应用和服务都是高度有状态的。

银行应用希望用户在允许用户查看其帐户相关资源之前登录。当每个无状态请求到达私有资源时，应用希望确保用户已经过身份验证。另一个例子是当用户想在一个三页的向导中打开一个帐户并填写表单时。在允许用户提交第二页之前，应用需要确保向导的第一页已经完成。

幸运的是，在 web 应用中有许多存储状态的选项。一种方法是在传输给客户端的资源中嵌入状态，这样应用所需的所有状态都会在下一个请求中返回。这种方法通常需要一些隐藏的输入字段，并且最适合短期状态(比如在三页向导中移动所需的状态)。在资源中嵌入状态将所有状态都保留在 HTTP 消息中，因此这是一种高度可扩展的方法，但它会使应用编程复杂化。

另一种选择是将状态存储在服务器上(或服务器后面)。对于必须存在很长时间的状态，此选项是必需的。假设用户提交一个表单来更改他或她的电子邮件地址。电子邮件地址必须始终与用户相关联，因此应用可以获取新地址，验证地址，并将地址存储在数据库、文件中，或者调用 web 服务让其他人负责保存地址。

对于服务器端存储，许多 web 开发框架(如 ASP.NET)也提供了对“用户会话”的访问。会话可能存在于内存或数据库中，但开发人员可以在会话中存储信息，并在每个后续请求中检索信息。存储在会话中的数据仅限于单个用户(实际上是用户的浏览会话)，不能在多个用户之间共享。

会话存储有一个简单的编程模型，只适用于短期状态，因为最终服务器必须假设用户已经离开了站点或关闭了浏览器，并且服务器将丢弃会话。会话存储如果存储在内存中，会对可伸缩性产生负面影响，因为后续请求必须到达会话数据所在的服务器。一些负载平衡器通过实现“粘性会话”来帮助支持这种情况。

您可能想知道服务器如何跟踪用户来实现会话状态。如果两个请求到达服务器，服务器如何知道这是来自同一个用户的两个请求，还是两个不同的用户各自发出一个请求？

在网络的早期，服务器软件可能通过查看请求消息的 IP 地址来区分用户。然而，如今，许多用户使用网络地址转换生活在设备后面，由于这个和其他原因，您可以让多个用户有效地使用同一个 IP 地址。IP 地址不是区分用户的可靠技术。

幸运的是，有更可靠的技术。

## 标识和饼干

想要追踪用户的网站往往会求助于**cookie**。Cookies 是由 RFC 6265([http://tools.ietf.org/html/rfc6265](http://tools.ietf.org/html/rfc6265))定义的，这个 RFC 被恰当地命名为“HTTP 状态管理机制”。当用户第一次访问一个网站时，该网站可以使用 HTTP 头给用户的浏览器一个 cookie。然后，浏览器知道在它发送给站点的每个附加请求的标题中发送 cookie。假设网站在 cookie 中放入了某种独特的标识符，那么网站现在可以在用户提出请求时对其进行跟踪，并区分不同的用户。

在我们深入了解 cookies 的外观和行为之前，值得注意几个限制。首先，cookie 可以识别用户，因为您的 cookie 不同于我的 cookie，但是 cookie 不会验证用户。经过身份验证的用户通常通过提供用户名和密码等凭据来证明自己的身份。到目前为止，我们讨论的 cookies 只是给了我们一些唯一的标识符来区分不同的用户，并在向网站发出请求时跟踪用户。

其次，由于 cookies 可以跟踪用户正在做什么，因此在一些圈子里引起了隐私问题。一些用户会禁用浏览器中的 cookie，这意味着浏览器会拒绝服务器发送的任何 cookie 作为响应。当然，对于需要跟踪用户的网站来说，禁用 cookies 会带来一个问题，而且替代方案也很混乱。例如，“无 cookie 会话”的一种方法是将用户标识符放入 URL 中。无 Cookieless 会话要求网站给用户的每个网址都包含适当的标识符，并且网址变得更大(这就是为什么这种技术通常被称为“胖网址”技术)。

## 设置饼干

当一个网站想要给用户一个 cookie 时，它会在一个 HTTP 响应中使用一个`Set-Cookie`头。

```cs
  HTTP/1.1 200 OK
  Content-Type: text/html; charset=utf-8
  Set-Cookie: fname=Scott$lname=Allen;
      domain=.mywebsite.com; path=/
  ...

```

本示例中显示的 cookie 中有三个信息区域。这三个区域用分号(；).首先，有一个或多个名称-值对。这些名称-值对由美元符号($)分隔，看起来非常类似于查询参数如何格式化为 URL。在示例 cookie 中，服务器希望在 cookie 中存储用户的名字和姓氏。第二个和第三个区域分别是域和路径。我们稍后将回过头来讨论域和路径。

一个网站可以把它喜欢的任何信息放入一个 cookie 中，尽管有 4 KB 的大小限制。然而，许多网站只为用户输入一个唯一的标识符，也许是一个 GUID。除非加密，否则服务器永远不会信任存储在客户端的任何内容。是的，在 cookie 中存储加密数据是可能的，但是存储 ID 通常更容易。

```cs
  HTTP/1.1 200 OK
  Set-Cookie: GUID=00a48b7f6a4946a8adf593373e53347c;
      domain=.msn.com; path=/

```

假设浏览器被配置为接受 cookie，浏览器将在每个后续的 HTTP 请求中向服务器发送 cookie。

```cs
  GET ... HTTP/1.1
  Cookie: GUID=00a48b7f6a4946a8adf593373e53347c;
  ...

```

当标识到达时，服务器软件可以从内存数据结构、数据库或分布式缓存中快速查找任何相关的用户数据。您可以配置大多数 web 应用框架来操作 cookies 并自动查找会话状态。例如，在 ASP.NET，`Session`对象公开了一个读取和写入用户会话状态的简单应用编程接口。作为开发人员，我们永远不必担心发送`Set-Cookie`头，或者读取传入的 cookies 来找到相关的会话状态。在幕后，ASP.NET 将管理会话 cookie。

```cs
  Session["firstName"]  = "Scott";     // writing session state
  ...
  var lastName = Session["lastName"];  // reading session state

```

再次值得指出的是，存储在会话对象中的`firstName`和`lastName`数据是**而不是进入 cookie** 。cookie 只包含一个会话标识符。与会话标识符相关联的值在服务器上是安全的。默认情况下，会话数据进入内存中的数据结构，并保持活动状态 20 分钟。当会话 cookie 在请求中到达时，ASP.NET 将使用存储在 cookie 中的标识找到用户数据后，将正确的会话数据与`Session`对象相关联。如果没有带有会话标识的传入 cookie，ASP.NET 将创建一个带有`Set-Cookie`头的 cookie。

关于会话标识符的一个安全问题是，它们如何打开某人劫持另一个用户的会话的可能性。例如，如果我使用像 Fiddler 这样的工具来跟踪 HTTP 流量，我可能会看到一个`Set-Cookie`头来自一个内部有`SessionID=12`的服务器。我可能会猜测其他用户已经有了 11 的`SessionID`，并创建一个带有该 ID 的 HTTP 请求，只是为了看看我是否可以窃取或查看其他用户的 HTML。为了解决这个问题，大多数网络应用将使用大随机数作为标识符(ASP.NET 使用 120 位的随机性)。ASP.NET 会话标识符如下所示，这使得猜测其他人的会话标识变得更加困难。

```cs
  Set-Cookie: ASP.NET_SessionId=en5yl2yopwkdamv2ur5c3z45;
      path=/; HttpOnly

```

## 超级饼干

关于 cookies 的另一个安全问题是它们如何容易受到跨站点脚本攻击(XSS)。在 XSS 攻击中，恶意用户将恶意的 JavaScript 代码注入他人的网站。如果其他网站向其用户发送恶意脚本，恶意脚本可以修改或检查并窃取 cookie 信息(这可能导致会话劫持，甚至更糟)。

为了对抗这个漏洞，微软引入了`**HttpOnly**`标志(见上一个`Set-Cookie`例子)。`HttpOnly`标志告诉用户代理不允许脚本代码访问 cookie。cookie 只为“HTTP”而存在，即在每个 HTTP 请求消息的报头中传播出去。实现`HttpOnly`的浏览器不允许 JavaScript 在客户端读写 cookie。

## 饼干的种类

我们目前看到的饼干是**会话饼干**。会话 cookies 只存在于单个用户会话，当用户关闭浏览器时会被销毁。**持久 cookie**可以比单次浏览会话更持久，用户代理会将 cookie 存储到磁盘上。你可以关闭一台电脑，一周后再回来，去你最喜欢的网站，第一次请求仍然会有一个持久的 cookie。

两者唯一的区别是持久的 cookie 需要一个`Expires`值。

Set-Cookie:名称=值；到期时间= 2012 年 7 月 9 日星期一格林尼治时间 21:12:00

会话 cookie 可以向 cookie 显式添加`Discard`属性，但是如果没有`Expires`值，用户代理在任何情况下都应该丢弃该 cookie。

## Cookie 路径和域

到目前为止，我们已经说过，一旦网站设置了 cookie，cookie 就会随着后续的每一个请求传播到网站(假设 cookie 没有过期)。然而，并不是所有的 cookies 都会传播到每个网站。用户代理应该发送到站点的唯一 cookie 是由同一站点提供给用户代理的 cookie。将来自 Amazon.com 的 cookies 放在对 google.com 的 HTTP 请求中是没有意义的。这种行为只会带来额外的安全和隐私问题。如果您在对 www.server.com 请求的响应中设置了一个 cookie，则生成的 cookie 将只在对 www.server.com 的请求中传播。

web 应用还可以更改 cookie 的范围，将 cookie 限制在特定的主机或域，甚至特定的资源路径。网络应用使用`**domain**`和`**path**`属性控制范围。

```cs
  HTTP/1.1 200 OK
  Set-Cookie: name=value; domain=.server.com; path=/stuff
  ...

```

`domain`属性允许 cookie 跨子域。换句话说，如果您设置了来自 www.server.com 的 cookie，用户代理将只向 www.server.com 发送 cookie。上一个例子中的域还允许 cookie 传播到 server.com 域中的任何 URL，包括 images.server.com、help.server.com 和普通 server.com。您不能使用 domain 属性跨域，因此在对. server.com 的响应中将域设置为. microsoft.com 是不合法的，用户代理应该拒绝该 cookie。

`path`属性可以将 cookie 限制在特定的资源路径上。在前面的例子中，只有当请求网址指向`/stuff`或`/stuff`下方的位置(如`/stuff/images`)时，cookie 才会传播到 server.com 站点。当多个团队在不同的路径上构建 web 应用时，路径设置可以帮助组织 cookies。

## 饼干的缺点

Cookies 允许网站在客户端存储信息，这些信息将在随后的请求中返回到网站。web 开发的好处是巨大的，因为 cookies 允许我们跟踪哪个请求属于哪个用户。但是，饼干确实有一些我们已经谈到的问题。

正如我们前面提到的，cookie 容易受到 XSS 攻击，当网站(尤其是广告网站)使用**第三方 cookie**在互联网上跟踪用户时，cookie 也会受到不良宣传。第三方 cookie 是从不同于浏览器地址栏中的域设置的 cookie。第三方 cookies 有这个机会，因为许多网站在将页面资源发送回客户端时，会包含指向其他 URL 的脚本或图像的链接。转到其他网址的请求允许其他网站设置 cookies。

例如，server.com 的主页可以包含一个来源设置为 bigadvertising.com 的`<script>`标签。这使得 bigadvertising.com 可以在用户观看来自 server.com 的内容时发送一个 cookie。饼干只能回到 bigadvertising.com，但如果有足够多的网站使用 bigadvertising.com，那么大广告可以开始对个人用户和他们访问的网站进行简介。大多数网络浏览器都允许您禁用第三方 cookies(但默认情况下是启用的)。

然而，cookies 最大的两个缺点是它们如何干扰缓存，以及它们如何随每个请求传输数据。任何带有`Set-Cookie`头的响应都不应该被缓存，至少不应该缓存头，因为这会干扰用户识别并造成安全问题。此外，请记住，存储在 cookie 中的任何内容在网络中传播时都是可见的(对于持久 cookie，它位于文件系统中)。因为我们知道有很多设备可以监听和解释 HTTP 流量，所以 cookie 永远不应该存储敏感信息。即使是会话标识符也有风险，因为如果有人可以截取另一个用户的 ID，他或她就可以从服务器上窃取会话数据。

即使有这些缺点，饼干也不会消失。有时我们需要状态通过 HTTP 传输，cookies 以一种简单、透明的方式提供了这种能力。我们有时需要的另一个功能是验证用户的能力。接下来我们将讨论身份验证功能。

## 认证

身份验证过程迫使用户通过输入用户名和密码、电子邮件和个人识别码或其他类型的凭据来证明自己的身份。

在网络级别，身份验证通常遵循质询/响应格式。客户端将请求安全资源，服务器将要求客户端进行身份验证。然后，客户端需要发送另一个请求，并包含供服务器验证的身份验证凭据。如果凭据良好，请求将会成功。

HTTP 的可扩展性允许 HTTP 支持各种身份验证协议。在本节中，我们将简要地看一下前 5 名:基本、摘要、窗口、表单和 OpenID。在这五个协议中，只有两个在 HTTP 规范中是“官方的”——基本和摘要身份验证协议。我们先来看看这两个。

## 基本认证

使用基本身份验证，客户端将首先使用普通的 HTTP 消息请求资源。

```cs
  GET http://localhost/html5/ HTTP/1.1
  Host: localhost

```

Web 服务器将允许您配置对特定文件和目录的访问。您可以允许所有匿名用户访问，也可以限制访问，以便只有特定的用户或组可以访问文件或目录。对于前面的请求，让我们假设服务器被配置为只允许某些用户查看`/html5/`资源。在这种情况下，服务器将发出身份验证质询。

```cs
  HTTP/1.1 401 Unauthorized
  WWW-Authenticate: Basic realm="localhost"

```

`401`状态代码告诉客户端请求未经授权。`WWW-Authenticate`头告诉客户端收集用户凭证，然后重试。`realm`属性为用户代理提供了一个字符串，它可以用作保护区域的描述。接下来会发生什么取决于用户代理，但是大多数浏览器可以显示一个用户界面供用户输入凭据。

![](img/img008.png)

图 8:身份验证对话框

有了凭据，浏览器可以向服务器发送另一个请求。该请求将包括一个`Authorization`标题。

```cs
  GET http://localhost/html5/ HTTP/1.1
  Authorization: Basic bm86aXdvdWxkbnRkb3RoYXQh

```

授权头的值是客户端的用户名和密码，基本编码为 64。**默认情况下，基本身份验证是不安全的**，因为任何拥有 base 64 解码器并能查看消息的人都可以窃取用户密码。因此，如果不使用安全的 HTTP，基本身份验证很少使用，我们将在后面讨论。

由服务器来解码授权头，并使用操作系统或服务器上的任何凭据管理系统来验证用户名和密码。如果凭据匹配，服务器可以进行正常回复。如果凭证不匹配，服务器应该再次以`401`状态响应。

## 摘要认证

摘要式身份验证是对基本身份验证的改进，因为它不使用 base 64 编码传输用户密码(基本上是以纯文本形式传输密码)。相反，客户端必须发送密码的**摘要**。客户端使用 MD5 哈希算法和服务器在身份验证质询期间提供的随机数(随机数是用于帮助防止重放攻击的加密数字)来计算摘要。

摘要质询响应类似于基本身份验证质询响应，但在`WWW-Authenticate`报头中有来自服务器的附加值，用于加密功能。

```cs
  HTTP/1.0 401 Unauthorized
  WWW-Authenticate: Digest realm="localhost",
      qop="auth,auth-int",
      nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
      opaque="5ccc069c403ebaf9f0171e9517f40e41"

```

当安全 HTTP 不可用时，摘要式身份验证优于基本身份验证，但仍远非完美。摘要式身份验证仍然容易受到中间人攻击，即有人嗅探网络流量。

## Windows 身份验证

Windows 集成身份验证不是标准身份验证协议，但它在微软产品和服务器中很受欢迎。尽管许多现代浏览器(不仅仅是 Internet Explorer)都支持 Windows 身份验证，但它在互联网上或代理服务器所在的地方并不能很好地工作。你会发现它在有微软活动目录服务器的内部和内部网站上很常见。

Windows 身份验证依赖于 Windows 支持的底层身份验证协议，包括 NTLM 和 Kerberos。Windows 身份验证质询/响应步骤与我们已经看到的非常相似，但是服务器将在`WWW-Authenticate`头中指定`NTLM`或`Negotiate`(`Negotiate`是一种允许客户端选择 Kerberos 或 HTML 的协议)。

```cs
  HTTP/1.1 401 Unauthorized
  WWW-Authenticate: Negotiate

```

Windows 身份验证的优点是即使不使用安全的 HTTP 也是安全的，并且对于 Internet Explorer 的用户来说是不引人注目的。当受到服务器的质询时，IE 会自动对用户进行身份验证，并且会使用用户用来登录 Windows 操作系统的凭据进行身份验证。

## 基于表单的身份验证

表单身份验证是互联网上最流行的用户身份验证方法。基于表单的身份验证不是标准的身份验证协议，不使用`WWW-Authenticate`或`Authorization`头。然而，许多 web 应用框架为基于表单的身份验证提供了一些现成的支持。

使用基于表单的身份验证，应用将通过将用户重定向到登录页面来响应匿名用户对安全资源的请求。重定向是一个 HTTP 302 临时重定向。通常，用户请求的网址可能包含在重定向位置的查询字符串中，这样一旦用户完成登录，应用就可以将用户重定向到他或她试图访问的安全资源。

```cs
  HTTP/1.1 302 Found
  Location: /Login.aspx?ReturnUrl=/Admin.aspx

```

基于表单的身份验证的登录页面是一个 HTML 表单，其中包含供用户输入凭据的输入。当用户单击提交时，表单值将`POST`发送到应用需要获取凭证并根据数据库记录或操作系统验证它们的目的地。

```cs
  <form method="post">
      ...
      <input type="text" name="username" />
      <input type="password" name="password" />
      <input type="submit" value="Login" />
  </form>

```

请注意，基于表单的身份验证将以纯文本形式传输用户凭据，因此，与基本身份验证一样，基于表单的身份验证是不安全的，除非您使用安全的 HTTP。响应带有凭证的`POST`消息(假设凭证是好的)，应用通常会将用户重定向回安全资源，并且还会设置一个 cookie，指示用户现在已通过身份验证。

```cs
  HTTP/1.1 302 Found
  Location: /admin.aspx
  Set-Cookie: .ASPXAUTH=9694BAB... path=/; HttpOnly

```

对于 ASP.NET，身份验证票据(即`.ASPXAUTH` cookie 值)被加密和散列以防止篡改。但是，如果没有安全的 HTTP，cookie 很容易被拦截，因此会话劫持仍然是一个潜在的问题。然而，表单身份验证仍然很受欢迎，因为它允许应用完全控制登录体验和凭据验证。

## 开标

虽然基于表单的身份验证赋予应用对用户身份验证的完全控制，但许多应用不需要这种级别的控制。具体来说，应用不希望管理和验证用户名和密码(用户也不希望每个网站都有不同的用户名和密码)。OpenID 是分散认证的开放标准。使用 OpenID，用户向 OpenID 身份提供者注册，身份提供者是唯一需要存储和验证用户凭据的站点。周围有很多 OpenID 提供商，包括谷歌、雅虎和 Verisign。

当应用需要对用户进行身份验证时，它会与用户和用户的身份提供者一起工作。用户最终必须向身份提供者验证他或她的用户名和密码，但是由于加密令牌和秘密的存在，应用将知道身份验证是否成功。谷歌在其“谷歌账户用户联合登录”网页([https://developers.google.com/accounts/docs/OpenID](https://developers.google.com/accounts/docs/OpenID))上概述了这一过程。

虽然与表单身份验证相比，OpenID 提供了许多潜在的好处，但由于实现、调试和维护 OpenID 登录舞蹈的复杂性，它一直面临着缺乏采用的问题。我们不得不希望工具包和框架继续发展，使 OpenID 认证方法变得更加容易。

## 安全 HTTP

前面我们提到了自我描述的文本 HTTP 消息是网络的优势之一。任何人都可以阅读信息，了解里面的内容。但是，我们在网上发送了很多信息，我们不想让其他人看到。我们已经在本书中讨论了其中的一些场景。例如，我们不希望网络上的任何其他人看到我们的密码，但我们也不希望他们看到我们的信用卡号或银行账号。安全 HTTP 通过在消息开始在网络上传输之前加密消息来解决这个问题。

安全 HTTP 也被称为 HTTPS(因为它在 URL 中使用了`https`方案，而不是常规的`http`方案)。HTTP 的默认端口是端口 80，HTTPS 的默认端口是端口 443。浏览器将根据方案连接到适当的端口(除非它必须使用 URL 中存在的显式端口)。HTTPS 通过在网络协议栈中使用额外的安全层来工作。安全层存在于 HTTP 层和 TCP 层之间，其特点是使用传输层安全协议(TLS)或称为安全套接字层(SSL)的 TLS 前身。

![](img/img009.png)

图 9:安全的 HTTP 协议层

HTTPS 要求服务器拥有加密证书。证书在 HTTPS 通信建立期间被发送到客户端。该证书包括服务器的主机名，用户代理可以使用该证书来验证它是否真正在与它认为正在与之通话的服务器通话。使用公钥加密技术和证书颁发机构(如 Verisign)的存在使验证成为可能，这些机构将签署并保证证书的完整性。管理员必须从证书颁发机构购买和安装证书。

我们可以涵盖许多加密细节，但从开发人员的角度来看，了解 HTTPS 最重要的事情是:

*   **HTTPS 的所有流量都在请求和响应**中加密，包括 HTTP 头和消息体，以及 URL 中主机名之后的所有内容。这意味着路径和查询字符串数据以及所有 cookies 都被加密。HTTPS 防止会话劫持，因为没有窃听者可以检查消息并窃取 cookie。
*   **由于服务器证书，服务器被认证给客户端。**如果你正在通过 HTTPS 与 mybigbank.com 通话，你可以确定你的信息确实是发往 mybigbank.com，而不是某个在网络上安装代理服务器来拦截来自 mybigbank.com 的请求和欺骗响应流量的人
*   **HTTPS 不认证客户端。**如果应用需要知道用户的身份，它们仍然需要实现表单身份验证或前面提到的其他身份验证协议之一。HTTPS 确实使基于表单的身份验证和基本身份验证更加安全，因为所有数据都是加密的。有可能将客户端证书用于 HTTPS，客户端证书将以最安全的方式对客户端进行身份验证。然而，客户端证书通常不在开放的互联网上使用，因为没有多少用户会购买和安装个人证书。公司可能要求员工的客户端证书才能访问公司服务器，但在这种情况下，公司可以充当证书颁发机构，并向员工颁发他们创建和管理的证书。

HTTPS 确实有一些缺点，其中大部分都与表现有关。HTTPS 的计算成本很高，大型网站通常使用专门的硬件(SSL 加速器)来减轻网络服务器的加密计算负担。HTTPS 流量也不可能缓存在公共缓存中，但是用户代理可能会将 HTTPS 响应保存在自己的私有缓存中。最后，HTTPS 连接的建立成本很高，需要在客户端和服务器之间进行额外的握手来交换密钥，并确保每个人都使用正确的安全协议进行通信。持久连接有助于分摊这一成本。

最后，如果你需要安全的通信，你会心甘情愿地支付性能罚款。

## 我们在哪里？

在本章中，我们已经讨论了 cookies、身份验证和安全 HTTP。如果您已经阅读了所有五章，我希望您已经找到了一些有价值的信息，这些信息将帮助您编写、维护和调试网络应用。