使用安全的客户端会话构建简单且可扩展的 Node.JS 应用程序 – Node.JS 假期季,第 3 部分

这是 Mozilla 身份团队的《Node.JS 假期季系列》中总共 12 集的第 3 集。它涵盖了如何为可扩展的 Node.js 应用程序使用会话。

静态网站易于扩展。您可以对其进行大量缓存,并且无需在向最终用户提供此内容的各种服务器之间传播状态。

不幸的是,大多数 Web 应用程序需要保存一些状态才能为用户提供个性化体验。如果用户可以登录您的网站,那么您需要为他们保留会话。通常的做法是使用随机会话标识符设置 cookie,并在服务器上根据此标识符存储会话详细信息。

扩展有状态服务

现在,如果您想扩展该服务,基本上有三种选择

  1. 在所有 Web 服务器上复制该会话数据,
  2. 使用每个 Web 服务器都连接到的中央存储,或
  3. 确保给定用户始终访问同一 Web 服务器

这些都有缺点

  • 复制会带来性能成本并增加复杂性。
  • 中央存储会限制扩展并增加延迟。
  • 将用户限制在特定服务器会导致问题,当该
    服务器需要关闭时。

但是,如果您从另一个角度看待问题,则可以找到第四种选择:在客户端存储会话数据。

客户端会话

将会话数据推送到浏览器有一些明显的优势

  1. 数据始终可用,无论哪个机器为用户提供服务
  2. 服务器上没有要管理的状态
  3. Web 服务器之间无需复制任何内容
  4. 可以立即添加新的 Web 服务器

不过,存在一个关键问题:您无法信任客户端不会篡改会话数据。

例如,如果您在 cookie 中存储用户帐户的用户 ID,则该用户可以轻松更改该 ID,然后访问其他人的帐户。

虽然这听起来像是一个障碍,但有一种巧妙的解决方案可以解决此信任问题:将会话数据存储在防篡改包中。这样,就不需要相信用户没有修改会话数据。服务器可以对其进行验证。

在实践中,这意味着您使用服务器密钥对 cookie 进行加密和签名,以防止用户读取或修改会话数据。这就是 client-sessions 的作用。

node-client-sessions

如果您使用 Node.JS,则可以使用一个库,该库使开始使用客户端会话变得非常简单:node-client-sessions。它替换了 Connect 的内置 sessioncookieParser 中间件。

以下是如何将其添加到 简单的 Express 应用程序

const clientSessions = require("client-sessions");

app.use(clientSessions({
  secret: '0GBlJZ9EKBt2Zbi2flRPvztczCewBxXK' // set this to a long random string!
}));

然后,您可以像这样在 req.session 对象上设置属性

app.get('/login', function (req, res){
  req.session.username = 'JohnDoe';
});

并将其读回

app.get('/', function (req, res){
  res.send('Welcome ' + req.session.username);
});

要终止会话,请使用 reset 函数

app.get('/logout', function (req, res) {
  req.session.reset();
});

立即撤销 Persona 会话

与服务器端会话相比,客户端会话的主要缺点之一是服务器不再能够销毁会话。

使用服务器端方案,只需删除存储在服务器上的会话数据就足够了,因为客户端上保留的任何 cookie 现在都将指向不存在的会话。但是,使用客户端方案,会话数据不在服务器上,因此服务器无法确定它是否已在每个客户端上删除。换句话说,我们无法轻松地将服务器状态(用户已注销)与存储在客户端上的状态(用户已登录)同步。

为了弥补此限制,client-sessions 会向 cookie 添加过期时间。在解包存储在加密 cookie 中的会话数据之前,服务器会检查它是否已过期。如果已过期,它将简单地拒绝使用它并将用户视为已注销。

虽然过期时间方案在大多数应用程序中都能正常工作(尤其是在将其设置为相对较低的值时),但在 Persona 的情况下,我们需要一种方法让用户在了解其密码已被泄露后立即撤销其会话。

这意味着需要在后端保留少量状态。我们 使这种即时撤销成为可能 的方法是在用户表以及会话 cookie 中添加一个新令牌。

现在,每个查看 cookie 的 API 调用也会从数据库中读取当前令牌值,并将其与 cookie 中的令牌进行比较。除非它们相同,否则将返回错误,并且用户将注销。

当然,此解决方案的缺点是每次 API 调用都需要额外读取数据库,但幸运的是,我们已经在大多数这些调用中读取了用户表,因此可以同时提取新令牌。

了解更多信息

如果您想试用 client-sessions,请查看此 简单的演示应用程序。然后,如果您发现任何错误,请通过 我们的错误跟踪器 告知我们。

系列中的先前文章

这是 关于 Node.js 的共 12 篇文章系列 的第三部分。前面的文章是

关于 Francois Marier

安全与隐私工程师

Francois Marier 的更多文章……

关于 Robert Nyman [荣誉编辑]

技术布道师和 Mozilla Hacks 编辑。发表有关 HTML5、JavaScript 和开放 Web 的演讲和博客。Robert 是 HTML5 和开放 Web 的坚定支持者,自 1999 年以来一直从事 Web 前端开发工作 - 在瑞典和纽约市。他还定期在 http://robertnyman.com 上发表博客,并且喜欢旅行和结识新朋友。

Robert Nyman [荣誉编辑] 的更多文章……


17 条评论

  1. Caio Ribeiro Pereira

    您好!这是一篇很棒的文章!

    我想知道我是否可以将 client-sessions 与 connect-redis(带有 redis 的会话/cookie 中间件)一起使用?

    https://github.com/visionmedia/connect-redis

    2012 年 12 月 5 日 05:21

    1. François Marier

      我从未尝试过将其与 connect-redis 一起使用,但如果您尝试了,请告诉我们结果如何!

      2012 年 12 月 5 日 17:11

  2. Framp

    这是一个非常棒的模块!

    我只有一个问题.. 您不能仅仅删除服务器端密钥以销毁会话吗?
    用户(或受损帐户场景中的攻击者)将在下次请求时简单地注意到它已断开连接。

    2012 年 12 月 5 日 07:00

    1. François Marier

      除非您像我们在 Persona 中那样使用第二个令牌,否则没有服务器端密钥可供删除,因为会话仅存在于客户端。

      2012 年 12 月 5 日 17:13

  3. David Mulder

    我在过去见过这种设置很多次,我个人(根本)不喜欢它。有很多与安全相关的理由:首先,我不相信加密应该是任何系统的主要安全机制。为什么?因为随着时间的推移,加密有失效的习惯。这可能永远不会永远正确,但现在已经正确了很多次了。例如,我不知道您是否为每个用户使用单独的加密密钥或共享的服务器密钥,但第一个是可以“破解”的(我见过它在现实生活中用于重要的应用程序,尽管在那里收集大量加密数据相对困难)。第二个应该没问题(目前)。无论哪种方式,这都纯粹是理论上的,例如量子计算机将对各种加密方案产生巨大影响,但这种特定应用程序不太可能在那时存在,但我想说的是,应用程序应该从根本上是安全的。让用户存储安全数据*不是*使应用程序从根本上安全的途径。

    哦,除此之外,我怀疑中央服务器的开销会大于不断将所有会话数据从客户端发送到服务器的开销(客户端上传*通常*很慢)。

    2012 年 12 月 5 日 13:31

    1. François Marier

      服务器密钥对每个用户都是相同的。这就是为什么您需要为其选择一个足够长且随机的字符串的原因。如果您这样做,则攻击者将无法在任何可行的时间范围内暴力破解您的密钥。

      当然,如果您有理由相信密钥已被泄露,则可以在任何时候轮换站点密钥。这样做将使所有现有会话失效。

      2012 年 12 月 5 日 17:18

    2. Simon

      我完全同意 David 的观点。

      实际上,在客户端存储数据允许任何人对加密数据进行大规模的暴力破解攻击。

      因此,管理员需要
      – 设置强大的加密(我认为 512 位是最小值)
      – 跟踪技术的进步,始终保持领先地位。
      – 希望不会出现突然的突破(量子 CPU……)破坏您的加密。

      而强大的加密会增加 CPU 的负担。

      2013 年 1 月 15 日 11:26

  4. Erlend Oftedal

    客户端会话还有两个额外的问题。
    1. 如果您在会话中存储了过多的数据,则会超过 HTTP 请求可以具有的最大标头大小。因此,每个请求都失败,对于大多数用户而言,恢复意味着关闭浏览器。
    2. 客户端会话容易出现竞争条件。请求 A 速度很慢,并且在执行请求 B 时,请求 B 会在其响应中修改会话。如果 A 现在完成并且也更新了会话,则它没有看到 B 的更改,然后该更改就会丢失。

    2012 年 12 月 5 日 15:50

    1. Lloyd Hilaiel

      这两个问题都是非常真实的。我们遇到了问题 2,诊断起来并不容易。您可以通过路径范围限定 cookie、努力实现单页面应用程序设计以及注意不必要的会话写入来缓解此问题。

      但是,如果您必须扩展 - 消除跨数据中心同步会话数据的需要以及低延迟要求在简化扩展方面具有巨大的意义。

      我想说,鉴于这些挑战,很难改造现有的应用程序 - 但我们在设计之初就非常早地使用了客户端会话,并且鉴于我们的扩展目标,这对我们来说很有意义。YMMV!

      2013 年 2 月 2 日 00:37

  5. gggeek

    Erlend 和 Simon 突出显示了不错的缺点。有充分的理由导致没有很多网站实现客户端会话……

    关于 Erlend 描述的竞争条件:大多数 Web 应用程序是否无论如何都会遇到同样的问题?我想,响应请求 A 的进程仅在其开始时读取会话数据(从数据库到内存),当它试图在结束时更新它时,它会将其存储(到数据库)而不检查在此期间是否有任何更新。除非还与会话数据一起存储了一些递增计数器,并且每次更新都会递增,这可以进行检查

    2013 年 1 月 30 日 13:16

    1. Erlend Oftedal

      不一定。这取决于会话的处理方式。我在一个经常更新会话的 Rails 应用程序中遇到了这个问题。当我将会话从客户端移到内存中时,一切按预期工作。
      对于内存存储,您可能会遇到其他问题。如果另一个请求正在更新它,则一个请求可能会看到会话的不一致视图。
      对于数据库存储的会话,可以通过事务正确处理会话,但这可能会影响性能。通常,数据库存储的会话也会被缓存,并且可以在不锁定的情况下进行更新。在这种情况下,它们可能会遇到与内存存储相同的问题。

      在我看来,由于多个请求导致的竞争条件的可能性大于脏读的可能性。

      2013 年 1 月 31 日 02:47

      1. Lloyd Hilaiel

        是的。我总结如下

        服务器端会话:会话更新在请求到达服务器时立即发生。

        客户端会话:会话更新在收到响应时发生。

        因此,规则是 - 可能会更改会话的 HTTP 请求不能并行化 - 这会严重影响前端设计。

        2013 年 2 月 2 日 00:43

  6. NodeDude

    不错的模块

    我不相信加密。假设会话持续 2 天,这足够时间来收集一些亚马逊计算盒并破解它。

    如果我猜对了,50,000 美元可以为您提供足够的火力,在 2 天内(会话过期)获取所有用户的根权限。这是一种不值得承担的风险。

    2013 年 3 月 13 日 07:27

  7. Erlend

    这取决于加密算法……
    http://www.eetimes.com/design/embedded-internet-design/4372428/How-secure-is-AES-against-brute-force-attacks-

    2013年3月13日 上午08:52

  8. NodeDude

    我对加密几乎一无所知。

    我真的很想了解一些详细的解释,或者只是关于在客户端存储加密会话的可行性的随意讨论。

    非常有吸引力的想法,但是有任何实际应用案例吗?
    有哪些可用的加密方法,以及哪种方法最适合这种实现?
    对于所选的加密策略,有哪些可用的解密方法?

    这个想法看起来既冒险又简单干净……=)

    2013年3月13日 上午11:36

  9. 是谁做的

    是否可以在创建后将cookie的过期时间设置为null,以便我可以同时提供永久cookie和会话cookie?

    2013年3月20日 下午13:35

  10. Steven

    我不得不考虑一个类似的方案。我不会使用令牌字符串,而是使用普通的时间戳。将创建日期存储在cookie中(无论如何我都要这样做)并在用户表中存储一个日期。当用户点击“撤销会话”时,用户表中的日期将更新为“现在”。比较时间戳以查看服务器是否应该接受cookie。

    2013年4月3日 上午08:52

本文评论已关闭。