SPA身份验证和会话管理的最佳实践

388

当使用Angular、Ember、React等框架构建单页应用程序时,人们认为什么是一些身份验证和会话管理的最佳实践?我可以想到几种方法来考虑解决这个问题。

  1. 将其视为与常规Web应用程序相同的身份验证,假设API和UI具有相同的源域。

    这可能涉及使用会话cookie、服务器端会话存储以及一些会话API端点,经过身份验证的Web UI可以调用该端点以获取当前用户信息,以帮助个性化或甚至在客户端确定角色/能力。当然,服务器仍将强制执行保护数据访问的规则,UI只是使用此信息自定义体验。

  2. 像使用公共API的任何第三方客户端一样处理,并使用类似于OAuth的令牌系统进行身份验证。客户端UI将使用此令牌机制对每个向服务器API发出的请求进行身份验证。

我并不是很专业,但#1似乎完全足够适用于绝大多数情况,但我真的很想听取更有经验的意见。


我更喜欢这种方式,https://dev59.com/TWcs5IYBdhLWcg3w437H#19820685 - allenhwkim
3个回答

559
这个问题在这里已经得到了解决,稍微有些不同的形式:

RESTful Authentication

但是这只涉及到服务器端。让我们从客户端的角度来看看这个问题。不过,在此之前,有一个重要的前提条件:

Javascript Crypto is Hopeless

Matasano关于它的文章很有名,但其中所包含的教训非常重要:

https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/august/javascript-cryptography-considered-harmful/

总结一下:
  • 中间人攻击可以轻易地用<script> function hash_algorithm(password){ lol_nope_send_it_to_me_instead(password); }</script>替换你的加密代码。
  • 对于通过非SSL连接提供任何资源的页面,中间人攻击很容易。
  • 一旦使用SSL,你就正在使用真正的加密。
并添加一个我的推论:
  • 成功的XSS攻击可能导致攻击者在客户端浏览器上执行代码,即使你正在使用SSL——所以即使你已经把每个事情都做得很好了,如果攻击者找到了一种方法在别人的浏览器上执行任何javascript代码,你的浏览器加密仍然可能失败。
这使得许多RESTful身份验证方案在使用JavaScript客户端时变得不可能或愚蠢。让我们看看!

HTTP基本认证

首先是HTTP基本认证。最简单的方案:只需在每个请求中传递名称和密码即可。
当然,这绝对需要 SSL,因为您正在通过每个请求传递一个 Base64(可逆)编码的用户名和密码。任何监听线路的人都可以轻松提取用户名和密码。大多数“Basic Auth 不安全”的论点来自于“HTTP 上的 Basic Auth”,这是一个可怕的想法。
浏览器提供了内置的 HTTP Basic Auth 支持,但它非常丑陋,您可能不应该在应用程序中使用它。另一种选择是将用户名和密码存储在 JavaScript 中。
这是最符合 RESTful 的解决方案。服务器不需要任何状态知识,并验证与用户的每个交互。一些 REST 爱好者(主要是稻草人)坚持认为,维护任何形式的状态是异端邪说,如果您考虑任何其他身份验证方法,他们会口吐白沫。这种符合标准的理论优势是有的 - 它被 Apache 支持,您可以将对象存储为由 .htaccess 文件保护的文件夹中的文件,如果您的心愿意!
问题?您在客户端缓存用户名和密码。这使得恶意网站更容易破解 - 即使最基本的XSS漏洞也可能导致客户端将其用户名和密码发送到恶意服务器。您可以尝试通过哈希和加盐密码来减轻此风险,但请记住:JavaScript Crypto是无望的。您可以通过将其留给浏览器的基本身份验证支持来减轻此风险,但......如前所述,丑陋。

HTTP摘要认证

jQuery是否支持摘要认证?

一种更“安全”的身份验证方式,它是一个请求/响应哈希挑战。但是JavaScript Crypto是无望的,因此它仅适用于SSL,并且您仍然必须在客户端上缓存用户名和密码,使其比HTTP基本身份验证更复杂但不更安全

使用附加签名参数进行查询身份验证。

另一种更“安全”的身份验证方式是使用nonce和时间数据加密参数(以防止重复和定时攻击),然后发送它们。其中最好的例子是OAuth 1.0协议,据我所知,这是在REST服务器上实现身份验证的一种非常出色的方法。

https://www.rfc-editor.org/rfc/rfc5849

哦,但是 JavaScript 没有任何 OAuth 1.0 的客户端。为什么?

JavaScript 加密是无望的,请记住。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/

攻击者无法获得用户的凭据,但他们仍然可以冒充您的用户,这可能非常糟糕。

简而言之:通过未加密的方式发送令牌意味着攻击者可以轻松地抓取这些令牌并冒充您的用户。FireSheep是一个使这变得非常容易的程序。

一个单独、更安全的区域

您运行的应用程序越大,就越难以绝对确保它们无法注入一些代码来更改您处理敏感数据的方式。您绝对信任您的CDN吗?您的广告商?您自己的代码库?

常见的信用卡详细信息,不太常见的用户名和密码 - 一些实施者将“敏感数据输入”保留在与其应用程序其他部分不同的页面上,这个页面可以被严密控制和封锁,最好是难以钓鱼用户的页面。
Cookie(只是令牌的意思)
在cookie中放置身份验证令牌是可能的(也很常见)。这不会改变令牌身份验证的任何属性,它更多的是方便性问题。所有先前的论点仍然适用。
会话(仍然只是令牌的意思)
会话认证只是令牌认证,但有一些不同之处,使它看起来像是稍微不同的东西:
- 用户从未经身份验证的令牌开始。 - 后端维护一个与用户令牌相关联的“状态”对象。 - 令牌提供在cookie中。 - 应用程序环境为您抽象了细节。
除此之外,它与令牌身份验证没有什么区别。
这甚至偏离了RESTful实现的更远 - 随着状态对象,您正在走向基于有状态服务器的普通RPC的道路。
OAuth 2.0
OAuth 2.0解决了“软件A如何让软件B访问用户X的数据,而无需让软件B访问用户X的登录凭据”的问题。实现方式基本上只是用户获取令牌的标准方式,然后第三方服务会确认“是的,这个用户和这个令牌匹配,现在你可以从我们这里获取他们的一些数据”。但从根本上说,OAuth 2.0只是一个令牌协议。它具有其他令牌协议的相同属性-您仍然需要SSL来保护这些令牌-它只是改变了生成这些令牌的方式。OAuth 2.0有两种帮助您的方式:向他人提供身份验证/信息和从他人那里获取身份验证/信息。但归根结底,您只是使用令牌。回到您的问题,所以您要问的问题是“我应该将我的令牌存储在cookie中,并让我的环境自动会话管理处理细节,还是将我的令牌存储在Javascript中并自行处理这些细节?”答案是:“做让您开心的事情”。
自动会话管理的问题在于,背后有很多神奇的事情正在为您发生。通常最好掌控这些细节。

我已经21岁了,所以SSL是可以使用的

另一个答案是:对所有内容使用https,否则强盗将窃取您用户的密码和令牌。


4
很好的回答。我很欣赏标记身份验证系统和基本 cookie 身份验证之间的等价性(通常内置于 Web 框架中)。这正是我在寻找的。我也很感激你涵盖了许多需要考虑的潜在问题。干杯! - Chris Nicola
14
我知道已经有一段时间了,但我想知道是否应该将其扩展以包括JWT?https://auth0.com/blog/2014/01/07/angularjs-authentication-with-cookies-vs-token/ - Chris Nicola
30
Token,“它也不太符合REST规范,因为令牌构成了“状态并使服务器实现更加复杂”。” (1) REST要求服务器无状态。客户端存储的令牌对服务器没有任何有意义的状态表示。(2)稍微复杂一些的服务器端代码与REST无关。 - soupdog
17
我很喜欢这个函数的名字 :D,请把它发送给我,而不是他。 - Leo
20
你似乎忽略了一件事:当标记为httpOnly时,Cookie是XSS安全的,并且可以通过secure和samesite进一步锁定。而且,处理Cookie已经存在很长时间,更加经过实战考验。依赖JS和本地存储来处理令牌安全性是愚蠢的游戏。 - Martijn Pieters
显示剩余7条评论

69
使用JWT(JSON Web Tokens)和SSL/HTTPS可以提高身份验证过程的安全性。 基本身份验证/会话ID可能会被盗用,原因包括: - 中间人攻击(未使用SSL/HTTPS); - 入侵者访问用户计算机; - 跨站脚本攻击(XSS)。
使用JWT时,您将用户的身份验证细节加密并存储在客户端中,并随每个请求一起发送到API,服务器/API会对令牌进行验证。 请注意,私钥是服务器/API保密存储的,因此它不能被解密/读取。
更安全的流程如下: 登录 - 用户通过SSL/HTTPS登录并向API发送登录凭据; - API接收登录凭据; - 如果有效: - 在数据库中注册新会话; - 使用私钥将用户ID、会话ID、IP地址、时间戳等信息加密为JWT; - API将JWT token发送回客户端(通过SSL/HTTPS); - 客户端接收JWT token并将其存储在localStorage/cookie中。
每个API请求: - 用户将HTTP请求与存储的JWT token一起发送到API(通过SSL/HTTPS); - API读取HTTP头并使用私钥解密JWT token; - API验证JWT token,将HTTP请求中的IP地址与JWT token中的IP地址匹配,并检查会话是否已过期; - 如果有效: - 返回请求内容; - 如果无效: - 抛出异常(403/401); - 标记系统入侵; - 向用户发送警告电子邮件。

JWT负载/声明可以在没有私钥(密钥)的情况下读取,并且将其存储在localStorage中不安全。对于这些错误的说法我感到抱歉。然而,他们似乎正在研究一种JWE标准(JSON Web Encryption)

我通过在JWT中存储声明(userID, exp),使用API /后端知道的私钥(密钥)对其进行签名,并将其作为安全的HttpOnly cookie存储在客户端上实现了这一点。这样就可以防止通过XSS读取它并且不能被篡改,否则JWT会失败签名验证。此外,通过使用安全的HttpOnlycookie,您确保仅通过HTTP请求发送cookie(不可通过脚本访问),并且仅通过安全连接(HTTPS)发送。

更新于17.07.16:

JWT是天生无状态的。这意味着它们会自动失效/过期。将SessionID添加到令牌的声明中,使其具有状态,因为其有效性不仅取决于签名验证和到期日期,而且还取决于服务器上的会话状态。但好处是,您现在可以轻松地使令牌/会话失效,而以前使用无状态JWT是不可能的。


3
从安全角度来看,最终JWT仍然只是“一个令牌”。服务器仍然可以将用户ID、IP地址、时间戳等信息与不透明的会话令牌关联起来,这样做与使用JWT并没有更安全或更危险的区别。然而,JWT的无状态特性确实使得实现更加容易。 - James
2
@James,JWT具有可验证性和携带关键细节的优点。这对于各种API场景非常有用,例如需要跨域身份验证的情况。会话并不像JWT那样好用。它也是一个已定义(或至少正在进行中)的规范,这对于实现非常有用。这并不意味着它比其他好的令牌实现更好,但它是明确定义和方便的。 - Chris Nicola
2
@Chris 是的,我同意你的所有观点。然而,上面回答中描述的流程并不是因为使用JWT而本质上更安全的流程。此外,除非您将标识符与JWT关联并在服务器上存储状态,否则在上述方案中JWT是不可撤销的。否则,您需要定期通过请求用户名/密码来获取新的JWT(用户体验差),或者发行具有非常长过期时间的JWT(如果令牌被盗,则很糟糕)。 - James
2
我的答案并不是100%正确的,因为JWT实际上可以在没有私钥(密钥)的情况下被解密/读取,并且将其存储在localStorage中是不安全的。我通过将声明(userID、exp)存储在JWT中,在API/backend使用私钥(密钥)进行签名,并将其作为HttpOnly cookie存储在客户端上来实现这一点。这样就无法通过XSS读取它。但是,您必须使用HTTPS,因为令牌可能会被MITM攻击窃取。我将更新我的答案以反映这一点。 - Gaui
1
@vsenko 每个请求都会从客户端发送 cookie。您无法从 JS 访问 cookie,它与客户端向 API 发送的每个 HTTP 请求相关联。 - Gaui
显示剩余15条评论

8
我会选择第二个,即令牌系统。
你知道ember-authember-simple-auth吗?它们都使用基于令牌的系统,就像ember-simple-auth所述:
“一个轻量级且不显眼的库,用于在Ember.js应用程序中实现基于令牌的身份验证。 http://ember-simple-auth.simplabs.com
它们有会话管理,并且也很容易插入现有项目。
还有一个Ember App Kit版本的ember-simple-auth示例:使用ember-simple-auth进行OAuth2身份验证的Ember App Kit工作示例。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接