内容


无状态的状态

记住五分钟前您在 Web 上所做的事

Comments

“状态” 和 “持久化” 是计算的关键术语。这些概念对于计算来说非常普遍,但是如果脱离上下文,就很难理解其含义。清楚地理解这些概念对于现代分布式应用程序的开发人员很重要。

总的来说,“状态” 是指关于程序当前的执行状况的信息 —— 存储在内存中的运行时数据。与之相反,“持久化” 是指在程序的各次执行之间保存数据。对于访问数据库中某个表的程序,数据库本身负责 “持久化”,而关于当前显示的行的信息就是 “状态”。

当 “状态” 这个词应用于协议时,是指每次执行的一系列交互具有连续性,就像程序的状态一样。而在 “无状态” 协议中就没有这种连续性;每个请求都是完全单独处理的。HTTP 就是一种无状态协议。

HTTP 的定义

传统上把 HTTP 称为无状态协议(参见 参考资料),意味着这或多或少是最明显的特征。HTTP 本质上由一个请求和一个响应组成:浏览器请求一个特定 URL(可能还提供补充数据),服务器用一个响应页面来应答。尽管最终用户可能觉得他们的网上冲浪过程由一系列连续的步骤组成,但是对于协议来说,每个交付的页面都是相互独立的;任何显示仅仅是与最近的 URL 请求对应的输出。

那么,Web 开发人员所说的 “会话”、“登录”、“注销”、“个性化”、“hijacking” 等有状态概念是如何实现的?其他一些设备补充了 HTTP,为它提供了状态功能;也就是说,服从 HTTP 定义的标准提供了可以解释为状态接口的机制(和其他功能)。

大多数 Web 框架和浏览器层高级接口(例如可编程的 Session 对象)简化了 Web 开发。但是,总的来说,它们都是把下面的会话维护机制封装在一个抽象接口中。

注意,详细讨论这些框架和接口超出了本文的范围。关于这些技术的更多信息,参见 参考资料

HTTP 交互混淆了状态和持久化之间的差异。开发人员可能把通过一次浏览会话保存的数据称为 “状态”,而把浏览会话之间保存的数据称为 “持久化”。另外,一些人可能把用户浏览器保存的数据称为 “状态”,把服务器保存的数据称为 “持久化”。在现代计算环境中,常常以不一致的方式使用基本术语。本文用 “状态” 表示客户机保存的数据,用 “持久化” 表示服务器保存的数据;这大致符合原始的 “cookie” 规范,这个规范把 cookie 描述为 “在客户端提供持久化状态”(参见 参考资料)。

客户端状态

可以在客户机程序中存储状态,而且这对于大多数应用程序是合适的。如果状态总是由客户机提供,服务器就是无状态的(至少从程序员的角度来看):不会维护数据,只需处理传入的数据。

cookie

“cookie” 规范是由 Netscape 在很久以前引入的。这个名称来自把标识符称为 “magic cookie” 的传统(MIT 的习惯)。cookie 可以把名称/值对与给定的 URL 关联起来,但是一般来说可以跨域设置它们。早期的浏览器容易受到攻击,黑客可以通过许多欺骗技术盗取个人信息,这使许多用户禁用了 cookie 或者定期删除 cookie。在默认情况下,cookie 会在浏览器关闭时过期;但是,指定了过期日期的 cookie 会一直存活到过期日期,即使用户关闭并重新打开浏览器,它们仍然是有效的。许多最佳实践规范建议不使用这些 “持久化” cookie。它们的可靠性不好(许多浏览器可以配置为在退出时清除所有 cookie,甚至包括持久化 cookie),而且会增加安全风险。如果必须使用它们,就要认识到它们不总是有效的。所以,在持久化 cookie 不可用的情况下,需要有其他解决方案。这意味着其实并不需要持久化 cookie。

尽管 cookie 现在由一个标准指定(RFC 2965,“HTTP State Management Mechanism” —— 参见 参考资料 中的链接),但是用户仍然不信任它们的功能,而且这种不信任常常是有道理的。cookie 本质上是不安全的,它们要求浏览器提供更多的安全限制。另外,cookie 以明文发送,所以在 cookie 中存储私密数据(比如用户名或密码)是很危险的。由于有这些安全问题,无法可靠地跨多个 Web 站点使用 cookie。许多浏览器拒绝处理不属于当前页面的域的 cookie。另外,cookie 不能区分共用一台计算机的多个用户,因此更危险。尽管如此,cookie 有时候是有用的。如果使用得当,它们可以作为状态处理机制的组成部分。如果对所有东西都使用 cookie,就不合适了。因为 cookie 不总是有效,所以还需要了解其他状态机制。

表单、方法和操作

过去有两种通过客户机提供状态的方法;这两种方法在表单上很常见。第一种方法是把状态信息附加到 URL 后面,作为 CGI 程序或其他程序的参数。第二种方法是使用 HTTP POST 提交参数。

在许多情况下,把参数附加到 URL 后面非常简单。另外,与 POST 参数相比,它有一个显著的优点:可以创建任意复杂的 URL 并在标准的链接标记中使用,不需要创建表单和提交按钮。

另一方面,POST 也有优点。最显著的优点之一是,POST 数据不会成为用户浏览器历史的一部分。另外,因为 HTTP 规范强烈建议 GET 操作应该是 “等幂” 的(多个获取操作应该只影响本身,而没有持久的影响),所以如果 GET 操作有副作用(比如用信用卡支付或取消预约),用户可能会非常吃惊。对于有副作用的任何东西,最好使用 POST。

当然,POST 参数和 URL 后缀都不安全,容易被编辑。有许多购物车应用程序把价格存储在用户可编辑的参数中,这导致了安全漏洞。另外,用户可能把 URL 做成书签或共享 URL,这对于依靠 URL 机制存储状态的系统非常不利。应该避免用 URL 存储状态。用 POST 数据存储状态要好一些,但是这要求用户会话中的每个链接都是一个表单提交。这两种方法都有显著的缺点。尽管它们对于表单数据和交互非常合适,但是不太适合维护状态。cookie 可能更好。

Ajax 和 XMLHttpRequest

在涉及 Web 访问时,使用 Ajax 或相似技术的页面用 “状态” 表示正在运行的程序的内部程序状态,这进一步模糊了状态和持久化数据之间的界限。基于 Ajax 的应用程序使用 JavaScript 变量控制状态;然后,用这些变量决定要提交的新请求。XMLHttpRequest 是 JavaScript(或其他脚本语言)用内部状态动态地更新 Web 页面的一种方式。

在实践中,许多用户禁用了 JavaScript 或限制了其部分功能,所以依靠 JavaScript 实现核心功能常常不合适。可移植性问题和不一致的实现也严重限制了 JavaScript 作为实现语言的作用。尽管 JavaScript 比较适合前沿用户,但是大量用户还在使用老式浏览器,JavaScript 不适合这些浏览器。

JavaScript 实现的安全漏洞可能给用户带来出乎意料的风险,他们可能因此怪罪您的 Web 站点。曾经有一些站点由于不谨慎的 JavaScript 编程方法而受到攻击。

插件和其他扩展

浏览器插件可以维护自己的状态,这些状态可能跨也可能不跨浏览器持久会话。Flash 插件过去使用 cookie(见下文),现在仍然可以这样做,但是又增加了对 “Local Shared Objects(LSO)” 的支持,LSO 有可能是很大的二进制对象(不仅仅是纯文本)。引入 LSO 似乎是为了存储游戏高分列表这样的东西,但是也可以用于其他许多用途。随着用户对安全问题越来越敏感,插件厂商也开始提供安全特性。一旦安全特性出现了,原来的一些数据存储方法就会被逐渐禁用或清除,cookie 目前的情况就是这样。

所有基于插件的机制都有一个缺点:用户可能没有安装这个插件,也可能无法获得这个插件。插件可能没有针对某一平台的版本,或者用户所在公司的 IT 部门不允许安装插件。Linux® 或 UNIX® 用户在使用依赖于浏览器插件的站点时可能会遇到困难。如果希望符合标准,就必须找其他办法。

插件的许多问题也出现在 “webOS” 上,webOS 尝试在 Web 上模拟桌面环境。当然,虚拟文件系统或相似的机制可以存储状态,但是用户必须有这些系统。

登录凭证

登录凭证也可以存储在浏览器中。这些登录凭证是简单的服务器安全系统所用的名称/密码对。HTTP 身份验证得到了广泛应用,可以提供一定的安全性,还能够存储少量状态。尽管这些登录凭证可以维护非常少的状态,但是对于大多数有状态应用程序是不够的。我在这里提到它们是因为它们有一个重要的用途:它们有助于控制对存储在服务器上的状态的访问。

与 cookie 一样,HTTP 身份验证凭证可以用来识别用户,而实际的状态管理在其他地方执行。但是,它们给用户操作造成了一定的困难,这常常是一个缺点,因为用户觉得很麻烦。用户常常被要求以某种方式清除他们对某个站点的 HTTP 身份验证凭证。另一方面,像这样使用登录凭证并不是对规范的滥用。通过身份验证之后,浏览器可能主动把存储的登录凭证发送给站点,而用户看不到登录提示。身份验证过程对用户是透明的。在一些浏览器中,可能出现像 HTTP 身份验证密码窗口一样的 JavaScript 警告框,这可不是好现象。

HTTP 身份验证的默认形式 “基本身份验证” 会以明文发送密码。这是不安全的(除非使用 SSL)。一种解决方案是使用数字摘要身份验证,它使用 MD5 散列值,比较难以泄密。在理想情况下,为了安全,应该通过 SSL 使用数字摘要身份验证。

另一个缺点是 HTTP 身份验证没有限制如何建立登录名和密码,它只控制如何使用登录名和密码。

服务器端状态

所有在客户机上存储状态的机制都有缺陷;最基本的缺陷是客户机可能丢失状态,导致服务器无法了解以前的情况。另外,客户机可以修改状态。因此,如果状态是由客户机提供的,服务器就必须检查每个事务中的所有状态信息。一种显而易见的解决方案是在服务器上存储状态。

为此,服务器使用 “会话” 的概念,会话是与单一用户的站点交互相关联的数据集合。Web 编程使用的语言常常提供会话管理特性。Perl 的 CGI::Session 模块、Ruby 的 CGI::Session 类(虽然名称相同,但是这两者没有关系)和 PHP 的 session_*() 函数都反映了这种设计需求。

持久化机制并不是惟一的体系结构决策。另一个重要的决策是,完整的状态如何在客户机和服务器端之间分配。实际上,想一想就会发现,要理解会话数据如何存储在服务器中是非常容易的。您实际上只需知道数据被可靠地存储,只需提供一个键(常常称为会话键或会话 ID),就可以访问会话数据。如果能够找到用户的会话 ID,就能够找到大量数据。

请考虑:“购物车” 数据应该放在浏览器上,还是在服务器上?

在前一种情况下,一旦服务器使用购物车提供的数据呈现完相应的页面后,就将丢弃这些数据。按照这种模型,浏览器负责记录您要购买的东西。如果浏览器会话丢失了,整个购物车也就消失了。在大多数情况下,这是一个缺点。

在后一种情况下,服务器在自己的数据库中记录您选择的商品,以会话 ID 作为索引。浏览器可能会崩溃,但是只要有 cookie 或书签,或者能够再次登录购物页面,就能够继续购物。

这种做法有显著的优点。数据可以存储在会话中,恶意用户无法直接修改它(当然,代码中的 bug 仍然可能产生安全漏洞)。重要的数据不需要通过因特网传输,尤其是不会以明文发送。但是,仍然需要传输会话键。

服务器端状态管理似乎是最合适的方法。服务器具备维护状态所需的大量资源。它所缺少的是一种完全可靠的查明哪个状态与给定查询相关联的方法。这听起来应该是客户端状态机制的任务。

记住您的会话 ID

我们能够在客户机上维护简单的状态,也能够在服务器上维护详细的复杂状态,所缺少的是它们之间的连接。这个连接点就是会话 ID。客户机只维护其中一种状态 —— 某种形式的会话标识符。然后,服务器可以使用这个标识符查找到所有复杂的状态(包括客户机应该无法访问的数据),并提供有状态的行为。

可以以几种方式管理会话 ID。可以用 cookie 存储会话 ID,可以把会话 ID 作为隐藏值嵌入在表单中,还可以把会话 ID 附加在 URL 后面。实际上,可以同时使用这三种方法,一些会话管理工具集会自动地尝试所有这些方式。我所知道的所有会话处理程序都自动实现这种行为。

这样就出现了浏览器会话的一致性问题,或者是有关 cookie 生命周期的问题。如果用户清除了私密的浏览器数据或者浏览器自动删除这些数据,会怎么样呢?会话存储在哪里呢?

这些问题引出了一种更高级的技术。数据通过用户名和密码键与某种用户帐户关联起来。当用户登录时,主要使用会话管理判断出用户已经登录;其他状态数据与用户帐户相关联,因此在断开连接或丢失状态之后,用户仍然可以重新获得状态。这样就跨越了从 “状态” 到 “持久化” 的界限。

这还可以更明确地区分临时设置(临时设置应该只在给定的一系列浏览器交互期间保存)和用户永久数据。

查明您的工具集所用的方法并尽可能了解其细节,有助于做出更好的决策,改进 Web 应用程序的安全性和性能。

其他有帮助的东西

正如前面提到的,状态机制可能会产生风险。如果用户能够访问别人的状态信息,会怎么样呢?有许多其他特性有助于减少维护 HTTP 状态带来的风险。本节讨论一些最常用的工具。

SSL/TLS

SSL 和相关技术有两个主要用途:在两方之间提供加密的通信,确认至少一方的身份。

cookie、HTTP 身份验证和各种在 URL 或表单中设置会话 ID 的方法都有一个缺点:在默认情况下,至少有一部分状态数据是明文的。使用 SSL/TLS 进行通信加密可以减少这种风险。另外,确认 Web 服务器的身份可以降低 “中间人” 攻击的风险;“中间人” 攻击可以盗用用户的凭证或利用给定会话中存储的信息。

常常使用 “HTTPS” 协议替代 HTTP 协议来管理安全套接字,HTTPS 协议实际上就是通过安全套接字的 HTTP。这导致许多需求。例如,HTTPS 使用另一个端口(但是大多数现代防火墙允许它通过)。但是,加密和身份的结合常常会给用户造成困难。不能只使用加密;您必须有一个身份证书,如果用户不信任签署证书的机构,浏览器就会发出警告。

IP 地址

在动态 IP 地址或代理环境中,IP 地址很难作为完全可靠的用户标识符,但是工具集可以使用 IP 地址作为检验用户身份或跟踪已经连接的新用户的辅助数据。宽带提供商常常尽可能保持用户的 IP 地址不变,但是 IP 地址仍然是不可靠的。

当然,IP 地址的最大优点是可以自动跟踪。毕竟,如果没有 IP 地址,服务器就无法响应请求。但是,代理服务器很可能让服务器看到的 IP 地址没有意义。尤其是,如果同一个 IP 地址上出现了另一个用户,不要因此认为原来用户的会话已经失效了。大型企业代理很可能让多个用户同时访问同一个站点。在实践中,IP 跟踪和其他方法的组合很有意义,比如收集统计数据,但是 IP 跟踪不适合作为可靠的状态跟踪机制。

HTTP Referrer

“Referrer” 常常被拼写成 “REFERER”,这个文法错误源自 Apache 默认的 404 错误消息。尽管这个拼写错误很讨厌,但是关于 referring 页面的信息常常有助于判断某个请求是否应当属于当前会话的一部分,还是新会话的开始。与 IP 地址一样,referrer 可以起辅助作用,但是不能替代真正的状态跟踪机制。它可以用来收集关于用户行为的统计数据。

注意,referrer 是很不可靠的:一些浏览器拒绝提供 referrer,一些浏览器会提供错误的 referrer。

结束语

最后,我想谈谈关于 Web 应用程序的乐观观点。对于许多应用程序,它们是 “足够好” 的。许多非技术人员(比如执行官或风险投资商)显然相信桌面应用程序或传统的客户机-服务器应用程序根本不需要重新部署。虽然这种想法过分乐观,但是确实说明了 Web 开发还有很长的路要走。

甚至一部分技术人员也认为 Web 应用程序更容易部署(参见 参考资料)。毕竟,Web 提供了一个自由安装的环境和众所周知的 API,而且大多数防火墙不会妨碍它。没有状态正是这个自由安装环境的基础。通过迫使开发人员几乎完全在服务器上解决持久化问题,而不依赖于客户机系统,缺少状态给开发人员提供了独立性。

但是,情况没这么简单。以前,由于缺少状态,Web 应用程序在执行长期或交互性过程方面遇到了许多困难。许多页面每次只执行一个任务,这导致非常复杂的表单和非常麻烦的错误检查。会话管理目前的水平反映了多年研发的成果。由于已经基本解决了这些问题,Web 应用程序现在已经无处不在了 —— 因为它们没有需求,不需要安装,不需要配置。经过多年研发所克服的缺点已经成为 Web 开发的 “杀手锏”。

NanoHUB 等项目展示了 Web 应用程序可以发展到什么程度(参见 参考资料)。如果希望通过 Web 提供与桌面应用程序相似的强大功能,确实是可以实现的 —— 但是,不像风险投资商和其他人想得是 “免费获得”。目前,大多数组织和公司还无法负担其成本(NanoHUB 的开发超过了 12 年)。

Web 应用程序的效果往往取决于浏览器的水平。浏览器开发人员需要跟上行业的发展步伐:可能应该先把创新放在一边,努力实现标准兼容,改进身份验证和 cookie 的安全性,实现真正的 “持久化 HTTP”。至少应该真正地实现数字摘要身份验证,停止使用基本身份验证(或者向用户发出警告)。作为开发人员,您必须了解这些限制。要照顾用户,不要给用户添麻烦;提示 “本站点需要 cookie” 并不是好方法,应该使用更可靠的恢复机制。

我们还有很长的路要走。浏览器开发人员需要实现更好的浏览器。会话管理工具需要更成熟和健壮。Web 框架可以提供更好更可靠的会话管理工具。太多人在重复编写自己的包装器来执行那些常见任务。应用程序开发人员和用户也需要做得更好。作为 Web 开发人员,您无法影响浏览器的水平,但是可以尽可能了解应用程序如何使用各种工具。了解安全问题出现在哪里,避免应用程序在不安全的通道上传输私密数据。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=328986
ArticleTitle=无状态的状态
publish-date=08122008