在使用 Angular、Ember、React 等框架构建 SPA 风格的应用程序时,人们认为哪些是身份验证和会话管理的最佳实践?我可以想出几种方法来考虑解决这个问题。
假设 API 和 UI 具有相同的源域,则与使用常规 Web 应用程序进行身份验证没有区别。这可能涉及到拥有会话 cookie、服务器端会话存储和一些会话 API 端点,经过身份验证的 Web UI 可以访问这些端点以获取当前用户信息,以帮助进行个性化,甚至可能确定客户端的角色/能力。当然,服务器仍然会强制执行保护数据访问的规则,UI 只会使用这些信息来定制体验。像使用公共 API 的任何第三方客户端一样对待它,并使用类似于 OAuth 的某种令牌系统进行身份验证。客户端 UI 将使用此令牌机制来验证对服务器 API 发出的每个请求。
我在这里并不是真正的专家,但对于绝大多数情况,#1 似乎完全足够了,但我真的很想听听一些更有经验的意见。
这个问题已在此处以稍微不同的形式详细解决:
但这从服务器端解决了它。让我们从客户端来看这个。不过,在我们这样做之前,有一个重要的前奏:
Javascript Crypto 是绝望的
Matasano 关于这方面的文章很有名,但其中包含的教训非常重要:
总结一下:
中间人攻击可以用
对于通过非 SSL 连接为任何资源提供服务的页面,中间人攻击是微不足道的。
拥有 SSL 后,无论如何您都在使用真正的加密货币。
并添加我自己的推论:
成功的 XSS 攻击可能导致攻击者在您的客户端浏览器上执行代码,即使您使用的是 SSL - 所以即使您已将每个舱口都关闭,如果您的攻击者找到执行方法,您的浏览器加密仍然会失败其他人浏览器上的任何 javascript 代码。
如果您打算使用 JavaScript 客户端,这会使许多 RESTful 身份验证方案变得不可能或愚蠢。我们看看吧!
HTTP 基本身份验证
首先,HTTP 基本身份验证。最简单的方案:只需在每个请求中传递一个名称和密码。
当然,这绝对需要 SSL,因为您在每个请求中都传递了 Base64(可逆)编码的名称和密码。任何在线收听的人都可以轻松提取用户名和密码。大多数“基本身份验证不安全”的论点都来自“基于 HTTP 的基本身份验证”,这是一个糟糕的想法。
浏览器提供了内置的 HTTP Basic Auth 支持,但它很丑陋,您可能不应该将它用于您的应用程序。不过,另一种方法是将用户名和密码存储在 JavaScript 中。
这是最 RESTful 的解决方案。服务器不需要任何状态知识,并验证与用户的每一次交互。一些 REST 爱好者(主要是稻草人)坚持认为,维护任何类型的状态都是异端邪说,如果您想到任何其他身份验证方法,它们都会在嘴边冒泡。这种标准合规性有理论上的好处——它由 Apache 开箱即用地支持——如果你愿意的话,你可以将你的对象作为文件存储在受 .htaccess 文件保护的文件夹中!
问题?您正在客户端缓存用户名和密码。这让 evil.ru 可以更好地破解它——即使是最基本的 XSS 漏洞也可能导致客户端将他的用户名和密码发送到恶意服务器。您可以尝试通过对密码进行散列和加盐来减轻这种风险,但请记住:JavaScript Crypto is Hopeless。您可以通过将其留给浏览器的基本身份验证支持来减轻这种风险,但是......如前所述,丑陋如罪。
HTTP摘要认证
Is Digest authentication possible with jQuery?
更“安全”的身份验证,这是一个请求/响应哈希挑战。除了 JavaScript Crypto 是无望的,所以它只能在 SSL 上工作,你仍然需要在客户端缓存用户名和密码,这使得它比 HTTP Basic Auth 更复杂,但并不更安全。
使用附加签名参数查询身份验证。
另一个更“安全”的身份验证,您可以在其中使用 nonce 和定时数据加密您的参数(以防止重复和定时攻击)并发送。最好的例子之一是 OAuth 1.0 协议,据我所知,这是在 REST 服务器上实现身份验证的一种非常棒的方式。
https://www.rfc-editor.org/rfc/rfc5849
哦,但是没有任何用于 JavaScript 的 OAuth 1.0 客户端。为什么?
记住,JavaScript Crypto 是绝望的。 JavaScript 无法在没有 SSL 的情况下参与 OAuth 1.0,并且您仍然必须在本地存储客户端的用户名和密码——这与 Digest Auth 属于同一类别——它比 HTTP Basic Auth 更复杂,但并不安全。
令牌
用户发送用户名和密码,作为交换获得可用于验证请求的令牌。
这比 HTTP Basic Auth 稍微安全一些,因为一旦用户名/密码事务完成,您就可以丢弃敏感数据。它也不太 RESTful,因为令牌构成“状态”并使服务器实现更加复杂。
SSL 静止
但问题在于,您仍然必须发送初始用户名和密码才能获得令牌。敏感信息仍然会触及您的可妥协 JavaScript。
为了保护您的用户凭据,您仍然需要阻止攻击者访问您的 JavaScript,并且您仍然需要通过网络发送用户名和密码。 SSL 必需。
代币到期
强制执行令牌策略很常见,例如“嘿,当这个令牌已经存在太久时,丢弃它并让用户再次进行身份验证。”或“我很确定允许使用此令牌的唯一 IP 地址是 XXX.XXX.XXX.XXX
”。其中许多政策都是非常好的想法。
火羊
但是,使用没有 SSL 的令牌仍然容易受到称为“sidejacking”的攻击:http://codebutler.github.io/firesheep/
攻击者没有获得您用户的凭据,但他们仍然可以伪装成您的用户,这可能非常糟糕。
tl;dr:通过网络发送未加密的令牌意味着攻击者可以轻松获取这些令牌并假装是您的用户。 FireSheep 是一个让这一切变得非常简单的程序。
一个独立的、更安全的区域
您运行的应用程序越大,就越难绝对确保他们无法注入一些代码来改变您处理敏感数据的方式。你绝对信任你的 CDN 吗?你的广告商?你自己的代码库?
信用卡详细信息很常见,而用户名和密码则不太常见 - 一些实施者将“敏感数据输入”保留在与其应用程序的其余部分不同的页面上,该页面可以尽可能严格控制和锁定,最好是很难钓鱼用户。
Cookie(仅表示 Token)
将身份验证令牌放在 cookie 中是可能的(并且很常见)。这不会使用令牌更改任何 auth 的属性,它更方便。之前的所有论点仍然适用。
Session(仍然只是意味着 Token)
Session Auth 只是 Token 身份验证,但有一些差异使它看起来有点不同:
用户从未经身份验证的令牌开始。
后端维护一个与用户令牌相关联的“状态”对象。
令牌在 cookie 中提供。
应用程序环境从您那里抽象出细节。
不过,除此之外,它与 Token Auth 并没有什么不同,真的。
这与 RESTful 实现相差甚远——使用状态对象,您将在有状态服务器上的普通 ol' RPC 路径上走得更远。
OAuth 2.0
OAuth 2.0 着眼于“软件 A 如何在软件 B 无法访问用户 X 的登录凭据的情况下让软件 B 访问用户 X 的数据”的问题。
该实现很大程度上只是用户获取令牌的标准方式,然后第三方服务会“是的,这个用户和这个令牌匹配,你现在可以从我们这里获取他们的一些数据。”
不过,从根本上说,OAuth 2.0 只是一个令牌协议。它具有与其他令牌协议相同的属性——您仍然需要 SSL 来保护这些令牌——它只是改变了这些令牌的生成方式。
OAuth 2.0 可以通过两种方式帮助您:
向他人提供身份验证/信息
从他人那里获取身份验证/信息
但归根结底,你只是......使用令牌。
回到你的问题
因此,您要问的问题是“我应该将我的令牌存储在 cookie 中并让我的环境的自动会话管理处理细节,还是应该将我的令牌存储在 Javascript 中并自己处理这些细节?”
答案是:做任何让你开心的事。
然而,关于自动会话管理的事情是,在幕后为您发生了很多神奇的事情。通常,自己控制这些细节会更好。
我 21 岁,所以 SSL 是的
另一个答案是:对所有事情都使用 https,否则强盗会窃取您用户的密码和令牌。
您可以通过使用 JWT(JSON Web 令牌)和 SSL/HTTPS 来提高身份验证过程的安全性。
基本身份验证/会话 ID 可以通过以下方式窃取:
MITM 攻击(中间人) - 没有 SSL/HTTPS
入侵者获得对用户计算机的访问权
跨站脚本
通过使用 JWT,您可以加密用户的身份验证详细信息并将其存储在客户端中,并将其与每个请求一起发送到 API,服务器/API 将在其中验证令牌。如果没有私钥(服务器/API 秘密存储)读取更新,则无法对其进行解密/读取。
新的(更安全的)流程将是:
登录
用户登录并将登录凭据发送到 API(通过 SSL/HTTPS)
API 接收登录凭据
如果有效:
在数据库中注册一个新会话读取更新
使用私钥对 JWT 中的用户 ID、会话 ID、IP 地址、时间戳等进行加密。
API 将 JWT 令牌发送回客户端(通过 SSL/HTTPS)
客户端收到 JWT 令牌并存储在 localStorage/cookie 中
对 API 的每个请求
用户使用 HTTP 标头中存储的 JWT 令牌向 API(通过 SSL/HTTPS)发送 HTTP 请求
API 读取 HTTP 标头并使用其私钥解密 JWT 令牌
API 验证 JWT 令牌,将来自 HTTP 请求的 IP 地址与 JWT 令牌中的 IP 地址相匹配,并检查会话是否已过期
如果有效:
返回带有请求内容的响应
如果无效:
抛出异常 (403 / 401)
标记系统中的入侵
向用户发送警告电子邮件。
更新 30.07.15:
JWT 有效负载/声明实际上可以在没有私钥(秘密)的情况下读取,并且将其存储在 localStorage 中是不安全的。我对这些虚假陈述感到抱歉。但是,他们似乎正在研究 JWE standard (JSON Web Encryption)。
我通过将声明(userID,exp)存储在 JWT 中来实现这一点,使用 API/后端只知道的私钥(秘密)对其进行签名,并将其作为安全的 HttpOnly cookie 存储在客户端上。这样它就不能通过 XSS 读取,也不能被操纵,否则 JWT 签名验证失败。此外,通过使用安全的 HttpOnly cookie,您可以确保 cookie 仅通过 HTTP 请求(脚本无法访问)发送并且仅通过安全连接 (HTTPS) 发送。
17.07.16 更新:
JWT 本质上是无状态的。这意味着他们使自己无效/过期。通过在令牌的声明中添加 SessionID,您将使其成为有状态的,因为它的有效性现在不仅取决于签名验证和到期日期,还取决于服务器上的会话状态。然而,好处是您可以轻松地使令牌/会话无效,这是以前使用无状态 JWT 无法做到的。
我会选择第二个,令牌系统。
您知道 ember-auth 或 ember-simple-auth 吗?它们都使用基于令牌的系统,例如 ember-simple-auth 状态:
一个轻量级且不显眼的库,用于在 Ember.js 应用程序中实现基于令牌的身份验证。 http://ember-simple-auth.simlabs.com
它们具有会话管理功能,并且也很容易插入到现有项目中。
还有一个 Ember App Kit 示例版本的 ember-simple-auth:Working example of ember-app-kit using ember-simple-auth for OAuth2 authentication.
不定期副业成功案例分享
It's also less RESTful, as tokens constitute "state and make the server implementation more complicated."
(1) REST 要求 服务器 是无状态的。存储在 客户端 的令牌不会以任何对服务器有意义的方式表示状态。 (2) 稍微复杂的服务器端代码与 RESTful 无关。lol_nope_send_it_to_me_instead
我喜欢这个函数的名字:D