在 Web 上,Web 服务器通常通过使用 SSL 或 TLS 以及服务器证书来对用户进行身份验证 - 但要验证用户是谁并不那么容易。SSL 和 TLS 确实支持客户端证书,但实际使用它们存在许多实际问题(例如,Web 浏览器不支持单一用户证书格式,并且用户发现安装它们很困难)。您可以从许多地方了解如何设置数字证书,例如 Petbrain。使用 Java 或 Javascript 有其自身的问题,因为许多用户禁用它们,一些防火墙会过滤掉它们,并且它们往往速度很慢。在大多数情况下,要求每个用户都安装插件也是不切实际的,但如果系统仅供相对较少数量用户的内网使用,则可能适用。
如果您正在构建内网应用程序,您通常应该使用用户使用的任何身份验证系统。类 Unix 系统倾向于使用 Kerberos、NIS+ 或 LDAP。您可能还需要处理基于 Windows 的身份验证方案(可以将其视为 Kerberos 和 LDAP 的专有变体)。因此,如果您的组织依赖 Kerberos,请将您的系统设计为使用 Kerberos。尝试将身份验证系统与应用程序的其余部分分离,因为组织可能会(将会!)随着时间的推移更改其身份验证系统。
许多技术不起作用或效果不佳。在某些情况下有效的一种方法是使用“基本身份验证”,它内置于几乎所有浏览器和服务器中。不幸的是,基本身份验证以未加密的方式发送密码,因此很容易窃取密码;基本身份验证本身真的只对无价值的信息有用。您可以将身份验证信息存储在用户选择的 URL 中,但在大多数情况下,您绝不应该这样做 - URL 不仅以不受保护的方式通过网络发送(与基本身份验证一样),而且还有太多其他方法可以使此信息泄露给他人(例如,通过许多浏览器存储的浏览器历史记录日志、代理日志以及通过 Referer: 字段泄露给其他网站)。您可以使用 SSL/TLS 连接(这将对其进行加密)包装与 Web 服务器的所有通信;这是安全的(取决于您的操作方式),如果您有重要数据,这是必要的,但请注意这在性能方面代价高昂。您也可以使用“摘要身份验证”,它会公开通信,但至少在不暴露用于验证用户的底层密码的情况下对用户进行身份验证。摘要身份验证旨在成为低价值通信的简单部分解决方案,但 Web 浏览器和服务器在互操作性方面并未广泛支持摘要身份验证。事实上,正如 2002 年 3 月 18 日 eWeek 文章中所述,微软的 Web 客户端 (Internet Explorer) 和 Web 服务器 (IIS) 错误地实现了标准 (RFC 2617),因此无法与其他服务器或浏览器一起使用。由于微软不认为这种不正确的实现是一个严重的问题,因此他们的绝大多数客户要等到有正确工作的程序还需要很长时间。
因此,当今 Web 上最常见的身份验证技术是通过 cookies。Cookies 并非真正为此目的而设计,但它们可以用于身份验证 - 但使用它们有很多错误的方法会造成安全漏洞,因此请小心。有关 cookies 的更多信息,请参阅 IETF RFC 2965 以及有关它们的旧规范。请注意,要使用 cookies,某些浏览器(例如,Microsoft Internet Explorer 6)可能会坚持要求您拥有隐私配置文件(在服务器的根目录中名为 p3p.xml)。
请注意,有些用户不接受 cookies,因此此解决方案仍然存在一些问题。如果您想支持这些用户,您应该通过 HTML 表单隐藏字段来回发送此身份验证信息(因为几乎所有浏览器都毫无问题地支持它们)。您将使用与 cookies 相同的方法 - 您只需使用不同的技术来将数据从用户发送到服务器。自然,如果您实施此方法,则需要包含设置以确保这些页面不会被缓存以供他人使用。然而,虽然我认为避免 cookies 更可取,但在实践中,这些其他方法通常需要更多的开发工作。由于对于许多应用程序开发人员来说,大规模实施此方法非常困难,因此我目前并未强调这些方法。我宁愿描述一种相当安全且相当容易实现的方法,而不是强调那些对于(开发人员或用户而言)难以正确实施的方法。但是,如果您可以毫不费力地做到这一点,请务必支持使用表单隐藏字段和加密链接(例如,SSL/TLS)发送身份验证信息。与所有 cookies 一样,对于这些 cookies,您应该启用 HttpOnly 标志,除非您有必须能够读取 cookie 的 Web 浏览器脚本。
Fu [2001] 讨论了 Web 上的客户端身份验证,以及一种建议的方法,而这正是我为大多数站点建议的方法。基本思想是将客户端身份验证分为两个部分:“登录过程”和“后续请求”。在登录过程中,服务器询问用户的用户名和密码,用户提供它们,服务器回复一个“身份验证令牌”。在后续请求中,客户端(Web 浏览器)将身份验证令牌发送到服务器(以及其请求);服务器验证令牌是否有效,如果有效,则服务请求。关于 Web 身份验证的另一个很好的信息来源是 Seifried [2001]。
某些 Web 身份验证技术的一个严重问题是它们容易受到称为“会话固定”的问题的影响。在会话固定攻击中,攻击者在用户甚至登录到目标服务器之前就固定了用户的会话 ID,从而消除了之后获取用户会话 ID 的需要。基本上,攻击者获取一个帐户,然后诱骗另一个用户使用攻击者的帐户 - 通常通过创建特殊的超文本链接并诱骗用户点击它。一篇描述会话固定的好文章是 Mitja Kolsek [2002] 的论文。您使用的 Web 身份验证系统应该能够抵抗会话固定。
登录过程通常以 HTML 表单的形式实现;我建议使用字段名称“username”和“password”,以便 Web 浏览器可以自动执行一些有用的操作。确保密码通过加密连接(使用 SSL 或 TLS,通过 https: 连接)发送 - 否则,窃听者可能会收集密码。确保所有密码文本字段在 HTML 中标记为密码,以便密码文本对于任何可以看到用户屏幕的人都不可见。
如果用户名和密码字段都已填写,请不要尝试自动以该用户身份登录。相反,显示带有用户和密码字段的登录表单;这使用户可以验证他们是否真的想以该用户身份登录。如果您未能做到这一点,攻击者将能够利用此漏洞执行会话固定攻击。偏执的系统可能想要简单地忽略密码字段并让用户填写它,但这会干扰可以为用户存储密码的浏览器。
当用户发送用户名和密码时,必须根据用户帐户数据库对其进行检查。此数据库不应“明文”存储密码,因为如果有人获得了此数据库的副本,他们会突然获得每个人的密码(并且用户经常重用密码)。有些人使用 crypt() 来处理这个问题,但 crypt 只能处理少量输入,所以我建议使用不同的方法(这是我的方法 - Fu [2001] 没有讨论这一点)。相反,用户数据库应存储用户名、盐和该用户的密码哈希值。“盐”只是一个随机字符序列,用于使攻击者即使获得了密码数据库也更难确定密码 - 我建议使用 8 个字符的随机序列。它不需要是密码学上随机的,只需与其他用户不同即可。密码哈希值应通过连接“服务器密钥 1”、用户密码和盐,然后运行密码学安全的哈希算法来计算。服务器密钥 1 是此服务器独有的密钥 - 将其与密码数据库分开保存。拥有服务器密钥 1 的人如果也拥有密码数据库,则可以运行程序来破解用户密码;由于它不需要记住,它可以是一个长而复杂的密码。最安全的是 HMAC-SHA-1 或 HMAC-MD5;您可以使用 SHA-1(大多数网站并不真正担心它允许的攻击)或 MD5(但 MD5 将是较差的选择;请参阅关于 MD5 的讨论)。
因此,当用户创建他们的帐户时,密码会被哈希处理并放入密码数据库中。当用户尝试登录时,声称的密码会被哈希处理,并与数据库中的哈希值进行比较(它们必须相等)。当用户更改密码时,他们应该输入旧密码和新密码,以及新密码两次(以确保他们没有输错);再次,确保这些密码的字符在屏幕上都不可见。
默认情况下,不要使用 cookies 将密码本身保存在客户端的 Web 浏览器上 - 用户有时可能会使用共享客户端(例如在咖啡店)。如果您愿意,您可以为用户提供在浏览器上“保存密码”的选项,但如果您这样做,请确保将密码设置为仅在“安全”连接上传输,并确保用户必须明确请求它(不要默认执行此操作)。
确保该页面被标记为不被缓存,否则代理服务器可能会将该页面重新提供给其他用户。
一旦用户成功登录,服务器需要以 cookie 的形式向客户端发送一个“身份验证令牌”,这将在下面描述。
一旦用户登录,服务器会向客户端发回一个带有身份验证令牌的 cookie,该令牌将从那时起使用。使用单独的身份验证令牌,这样用户无需不断登录,这样密码不会持续来回发送,并且如果需要,可以使用未加密的通信。建议的令牌(忽略会话固定攻击)看起来像这样
exp=t&data=s&digest=m |
这种方法存在潜在的弱点。我担心 Fu 的方法,正如最初描述的那样,在会话固定攻击面前很脆弱(来自几个不同的方向,我不想在这里深入探讨)。因此,我现在建议修改 Fu 的方法并使用此令牌格式代替
exp=t&data=s&client=c&digest=m |
这是一个例子。如果用户成功登录 foobar.com,您可以将过期日期设置为 2002-12-30T1800(假设我们暂时以此格式以 ASCII 文本传输),用户名为“fred”,客户端会话为“1234”,并且您可以确定客户端的 IP 地址为 5.6.7.8。如果您使用简单的 SHA-1 键控摘要(并使用密钥作为其余数据的前缀),服务器密钥 2 的值为“rM!V^m~v*Dzx”,则可以对以下内容计算摘要
exp=2002-12-30T1800&user=fred&session=1234&client=5.6.7.8 |
101cebfcc6ff86bc483e0538f616e9f5e9894d94 |
从那时起,服务器必须检查过期时间并重新计算此身份验证令牌的摘要,并且仅在摘要正确时才接受客户端请求。如果没有令牌,服务器应回复用户登录页面(带有一个隐藏的表单字段,以显示成功登录后应转到哪里)。
显示用户名是明智的,尤其是在重要屏幕上,以帮助对抗会话固定攻击。如果用户收到关于其用户名的反馈,他们可能会注意到他们是否没有他们期望的用户名。如果有可能出现意外的用户名(例如,一个家庭共用一台机器),这无论如何都是有帮助的。重要屏幕的示例包括上传应保密的文件时的屏幕。
一个奇怪的实现问题:尽管 cookies 的“Expires:”(过期时间)字段的规范允许时区,但事实证明,某些版本的 Microsoft Internet Explorer 没有为 cookie 过期正确实现时区。因此,您需要始终在 cookie 过期时间中使用 UTC 时间(也称为 Zulu 时间),以获得最大的可移植性。总的来说,对时间值使用 UTC 时间是个好主意,并在必要时转换为人类显示,因为这消除了其他时区和夏令时问题。
如果您在身份验证令牌中包含会话 ID,则可以进一步限制访问。您的服务器可以“跟踪”用户在给定会话中查看了哪些页面,并且仅允许从该点访问其他适当的页面(例如,仅限从这些页面直接链接的页面)。例如,如果用户被授予对页面 foo.html 的访问权限,并且页面 foo.html 指向资源 bar1.jpg 和 bar2.png,则可以拒绝访问 bar4.cgi。您甚至可以终止会话,但仅当身份验证信息有效时才这样做(否则,这将使攻击者有可能对其他用户造成拒绝服务攻击)。这将将在一定程度上限制攻击者拥有的访问权限,即使他们成功劫持了会话,但显然,拥有时间和身份验证令牌的攻击者可以像普通用户一样“浏览”链接。
一个决定是是否要求通过安全连接(例如,SSL)发送身份验证令牌和/或数据。如果您以明文(非安全)方式发送身份验证令牌,则拦截令牌的某人可以执行用户可以执行的任何操作,直到过期时间为止。此外,当您通过未加密的链接发送数据时,存在攻击者未注意到的更改的风险;如果您担心有人可能会在途中更改数据,则需要验证正在传输的数据。加密本身并不能保证身份验证,但它确实使损坏更容易被检测到,并且典型的库可以支持 TLS/SSL 连接中的加密和身份验证。一般来说,如果您要加密消息,您还应该对其进行身份验证。如果您的需求各不相同,一种替代方法是创建两个身份验证令牌 - 一个仅在“安全”连接中用于重要操作,而另一个用于不太重要的操作。确保用于“安全”连接的令牌被标记,以便仅使用安全连接(通常是加密的 SSL/TLS 连接)。如果用户之间没有真正的区别,则身份验证令牌可以完全省略“data”。
再次,确保带有此身份验证令牌的页面未被缓存。还有其他合理的方案;本文的目标是至少提供一种安全的解决方案。许多变体是可能的。