本文为翻译内容。原文链接:Authentication in SPA (ReactJS and VueJS) the right way - Part 1
cookies - session, token, JWT,在哪里存放token?需要考虑中的安全问题?如何防止攻击?这些问题都会了在下文中提及
系列内容:
- 第一部分:如何在你的SPA中存储 access token
- 第二部分:如何使用身份提供者来识别用户并在你的SPA中提供SSO(ODIC,OAuth2概念)
单页应用程序(SPA)中的身份验证涉及多种模式,各有利弊。 本文将列出在处理用户身份验证时,要了解并牢记的主要重要概念和最佳实践,尤其是在这种通用体系结构中:
安全相关的基础准备
加密通讯(https)
- 由于身份验证相关功能需要使用HTTP标头,并且在通讯过程中会传递高度敏感的数据(密码,访问令牌等),因此必须使用加密的方式进行通讯,否则,嗅探网络的人可能会捕捉到这些信息。
不要使用URL查询参数来传递敏感数据
- URL和URL查询参数可能会出现在服务器日志,浏览器日志,浏览器历史记录,站点引荐来源网址(site referrer)中:有人可以从那里获取数据并尝试重新使用它。
- 未经培训的用户可能会复制并粘贴带有身份验证令牌的URL,这可能会导致基本上无意的会话劫持。
- 你可能中遇到 url 的长度可能超出浏览器或者服务器的限制的情况
防止暴力攻击
- 攻击者可能会通过尝试多种可能性来尝试推断密码,令牌或用户名。
- 在您的后端服务器上实现频率限制功能,以限制尝试和重试的次数。
- 对命中过多服务器错误(300 +,400 +,500)的用户请求采取禁止或者暂缓(trapit)措施
- 不要提供有关您技术的线索,例如,清除X-Powered-By,它在响应头中说明您使用哪种服务器。 如果你运行的是ExpressJS,你可以使用Helmetjs来实现上述功能。
定期更新您的依赖项
-
为避免使用存在安全问题的软件包,你可能更新你的 NodeJS npm 软件包:
# 列出安全漏洞 npm audit # 使用 npm npm outdated npm update # 升级主要版本的工具 (包含潜在的破坏性更新) npm install -g npm-check-updates ## --- 使用 yarn # Upgrade of minor and patch version following your version ranges in package.json yarn outdated yarn update # Interactive upgrade of minor and patch version following your version ranges in package.json yarn upgrade-interactive # List outdated dependencies including major version yarn upgrade-interactive --latest -
另外,如果你不使用PaaS,请保持服务器最新。
添加监控
- 监视服务器,在事件发生之前,识别异常行为。
认证
在REST API上标识客户端的身份验证机制主要有两种(您将在后面看到,我们可以将它们组合在一起):
- Bearer Token
- Authentication cookie
身份验证是识别用户/客户端的动作。
Bearer Token
什么是 bearer token?
Bearer Token 只是一个值,一个会在每次 https 请求中放在 Authorization 请求头的值,它不会自动的存储下来, 没有过期时间,也与域名无关。
GET <https://www.example.com/api/users>
Authorization: Bearer my_bearer_token_value
使用curl命令行时,它大概这样传递的:
curl -s -X GET "<https://www.example.com/api/users>" -H "Authorization: Bearer my_bearer_token_value"
为了构建一个无状态(stateless)应用程序,我们可以使用JWT作为令牌格式。 简单地说,JWT包含3个部分:
- 头部(Header)
- 内容(Payload)内容里可以保存用户id,用户角色,以及过期时间等
- 签名(Signature)
JWT是一种交换信息的密码安全方法,可实现无状态身份验证。 由于对称或非对称(RSA)签名,token的签名证明可以证明token的内容没有被修改(即未被破坏)。 标头包含用于验证签名的格式和公共密钥地址(针对非对称加密的情况)。
基本上,客户端应用程序一旦通过用户/密码身份验证(或其他方式)进行身份验证,便会获得JWT令牌。在 Javascript 的帮助下,客户端可以在接下来的所有请求中将JWT令牌放在请求头中,将JWT令牌发送到服务器。 服务器验证签名是否与令牌中的内容(payload)匹配。如果存在匹配项,则服务器可以信任轮流转中的内容(payload)。
基本用例
- 保护浏览器与特定后端之间的流量
- 保护移动应用程序,桌面应用程序与特定后端之间的流量
- 保护由不同组织控制的后端(M2M)之间的流量(例如OpenId Connect),或保护组织内的各个后端服务之间的流量
在哪里存储JWT?
我们必须手动将JWT存储在客户端中(内存,本地/会话cookie,本地存储 (localStorage) 等)。
不建议将JWT存储在浏览器 localStorage 中:
- 如果用户关闭浏览器,它将保留下来,这样的话,在JWT过期之前,会话是可以被恢复的。
- 页面上的任何JavaScript代码都可以访问本地存储:它没有任何数据保护功能。
- web worker 无法使用它
将JWT存储在 session cookie 中可能是解决方案,我们将在后面讨论。
延伸阅读: auth0.com/docs/securi…
基本攻击
-
当JavaScript处理安全性时,跨站脚本(XSS)攻击是最常见的:攻击者可能会破坏网站JS依赖关系,或者使用用户输入来添加恶意的JavaScript代码来窃取受害者的JWT。 然后,攻击者将使用它来模拟用户。
-
例如,在博客评论上,用户可以在其评论中添加JS,以在页面上执行客户端JS:
<img src=x onerror="javasc& #0000114ipt:ale� 00114t('XSS')"> -
可以通过转义和控制用户生成的内容来消减XSS攻击,但要检测和缓解由公共CDN服务的受破坏的Web依赖关系将非常困难。
认证 cookie
Cookie是一个名称/值对,存储在Web浏览器中,并且具有到期日期和相关的域。 Cookies存储在网络浏览器中。 可以通过客户端浏览器JavaScript创建它们:
document.cookie = 'my_cookie_name=my_cookie_value'; // JavaScript
或从服务器返回的HTTP响应头:
Set-Cookie: my_cookie_name=my_cookie_value // HTTP Response Header
网络浏览器会自动将cookie和每个请求发送到cookie的域。
GET <https://www.example.com/api/users>
Cookie: my_cookie_name=my_cookie_value
在大多数(有状态的)用例中,cookie用于存储会话ID。 会话ID由服务器管理(创建和超时)。 我们说这个是有状态,因为服务器需要在服务器上管理状态,而JWT令牌是无状态的。
Cookie 共有2种(来源):
- 会话Cookie (Session Cookie):由于客户端未指定Expires或Max-Age指令而关闭,因此该Cookie被删除。 但是,Web浏览器可能会使用会话还原,这会使大多数会话Cookie保持永久状态,就像从未关闭过浏览器一样。 会话超时必须在服务器端处理。
- 永久性cookie (Permanent Cookie):永久性cookie不会在客户端关闭时过期,而是在特定日期(Expires)或特定时间长度(Max-Age)过期。
服务器cookie可以使用以下几种属性:
- HttpOnly cookie:浏览器javascript无法读取它们。
- Secure Cookie:仅在通过安全通道(通常为HTTPS)传输请求时,浏览器才会将Cookie包含在HTTP请求中。
- SameSite cookie:使服务器要求不应该将cookie与跨站点请求一起发送,从而在一定程度上防止了跨站点请求伪造攻击(CSRF)。 SameSite cookie 仍处于试验阶段,尚不受所有浏览器支持。【支持情况】
基本用例
- 保护浏览器和特定后端之间的流量
- Cookies使非基于浏览器的应用程序(例如移动或平板电脑应用程序)更难以使用您的API。
Cookie存放在哪里?
它们会自动与过期日期(可选)和关联的域一起存储在Web浏览器中。
基本攻击
-
跨站点脚本(XSS)(如果未使用
HttpOnly选项创建Cookie):攻击者可能注入了Javascript 代码,从而窃取了受害者的身份验证Cookie。 -
跨站点请求伪造(CSRF)是伴随身份验证Cookie的常见攻击。 可以在服务器端完成CORS(跨源资源共享)配置,以仅授权特定的主机名。 但是,检查CORS是在客户端浏览器上进行的
甚至更糟的是,CORS方案提供的相同来源策略仅适用于浏览器端编程语言。 因此,如果您尝试使用JavaScript提交内容(post)到与原始服务器不同的服务器,则将使用相同的原始策略,但是如果您直接从HTML提交表单,则操作可能指向其他服务器,例如:
<form action="<http://someotherserver.com>">由于在发布表单时不涉及任何JavaScript,因此“相同的来源政策”不适用,浏览器正在将Cookie与表单数据一起发送。
CSRF的另一个示例:假设用户仍在登录 facebook.com 时访问了 bad.com 上的页面。 现在,bad.com 属于攻击者,他在 bad.com 上编写了以下代码:
<img src="<https://facebook.com/postComment?userId=dupont_123&comment=I_VE_BEEN_HACKED>">
为了减轻XSS,必须在cookie上设置 HttpOnly 选项。
为了减轻CSRF,必须在cookie上设置 SameSite 选项。 并非所有浏览器都支持 SameSite 选项,因此不会阻止所有CSRF攻击。 可以使用其他一些缓解策略(可以组合使用):
- 会话超时时间短(在金融领域,超时时间约为10分钟或更短)
- 关键操作应始终要求用户提供认证信息(例如,在更改用户电子邮件地址时,要求用户输入密码)
- 双重提交的Cookie:用户访问网站时,该网站应生成一个(具有加密强度的)伪随机值,并该值设置为cookie(没有
HttpOnly标记,以便JS代码可以获取到该值);于此同时设置一个HttpOnly的 认证cookie。该站点应要求每个表单提交都将这个伪随机值不仅放在表单数据中,还要放在cookie中。当 POST 请求发送到站点时,仅当表单值和cookie值相同时才将请求视为有效。攻击者代表用户提交表单时,他只能修改表单的值。根据同源策略,攻击者无法读取从服务器发送的任何数据或修改cookie值。这意味着,尽管攻击者可以通过表单发送他想要的任何值,但他将无法修改或读取存储在cookie中的值。由于cookie值和表单值必须相同,因此攻击者将无法成功提交表单,除非他能够猜测伪随机值(源)或从伴随的XSS攻击中窃取该值。
我们可以两者结合吗?
让我们总结一下我们在服务器API上寻找身份验证机制的情况:
- 支持浏览器和 M2M (Machine to Machine) 请求
- 尽可能抵抗 XSS 和 CSRF 的攻击
- 如果可以的话使用无状态
将JWT放入Cookie中以同时兼顾两者的优点又如何呢?
我们的API应该支持来自请求标头的JWT以及会话cookie中的JWT。 如果我们想授权javascript读取JWT有效负载,则可以通过组合两种类型的cookie来使用“两个cookie身份验证组合”方法,从而限制了XSS攻击面。
三种情况
Peter Locke在这片文章中介绍了使用两个cookie进行身份验证的方法
服务器可以在每个请求中无缝地更新JWT,因为新的JWT会出现在cookie响应中,并由浏览器自动存储。 这样,可以缩短JWT的到期日期。
为了限制CSRF,进行内容修改时绝对不要使用GET请求,要使用PUT或POST。 具有高度安全隐患的变更应再次询问用户凭据,例如,更改电子邮件变更应询问用户密码以验证更改。 可以在临时cookie嵌入一个随机数,供JS读取,并随同表单数据一起提交到隐藏的表单域中。 服务器必须检查cookie中的随机数是否与表单数据中的值匹配。
使用随机数防止Cookie CSRF
总结
现在,我们的SPA的身份验证流程如下:
- 第一步:我们的SPA应用程序检查是否存在带有JWT有效负载的cookie,如果存在,则表明用户已通过身份验证,否则SPA重定向到/ login页面。 如果您使用单个
HttpOnlycookie,则SPA应该进行API调用,例如//backend/api/me,以了解谁是当前用户,如果缺少身份验证cookie(包含JWT),则会收到未授权的错误。 或无效。 - 第二步 - 方案一:前端的
/login页面询问用户凭证(登录名/密码),然后使用AJAX请求将其发布在后端API上。 AJAX响应将使用JWT设置身份验证cookie。 - 第二步 - 方案二:
/login页面使用OAuth流提供OpenID身份验证。 对于授权码授予流程,/login应该将整个浏览器窗口重定向到//backend/auth/。 应该完成OAuth流程,后端应该在最后一个响应中在JWT的内部设置身份验证cookie。 然后它将浏览器重定向到前端。 然后,SPA将再次启动,因此请再次执行步骤1。