如何处理SPA(react、vue)的用户认证 -- 第一部分

958 阅读11分钟

本文为翻译内容。原文链接: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="&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&
    #0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#00
    00114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041">
    
  • 可以通过转义和控制用户生成的内容来消减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页面。 如果您使用单个 HttpOnly cookie,则SPA应该进行API调用,例如//backend/api/me,以了解谁是当前用户,如果缺少身份验证cookie(包含JWT),则会收到未授权的错误。 或无效。
  • 第二步 - 方案一:前端的 /login 页面询问用户凭证(登录名/密码),然后使用AJAX请求将其发布在后端API上。 AJAX响应将使用JWT设置身份验证cookie。
  • 第二步 - 方案二:/login 页面使用OAuth流提供OpenID身份验证。 对于授权码授予流程,/login 应该将整个浏览器窗口重定向到//backend/auth/。 应该完成OAuth流程,后端应该在最后一个响应中在JWT的内部设置身份验证cookie。 然后它将浏览器重定向到前端。 然后,SPA将再次启动,因此请再次执行步骤1。