Node 中安全会话管理的最佳实践

2025-06-09

Node 中安全会话管理的最佳实践

在 Web 应用程序中,数据通过 HTTP 从浏览器传输到服务器。在现代应用程序中,我们使用 HTTPS 协议(即基于 TLS/SSL(安全连接)的 HTTP)来安全地传输数据。

纵观常见的用例,我们经常会遇到需要保留用户状态和信息的情况。然而,HTTP 是一个无状态协议。会话用于在 HTTP 请求之间存储用户信息。

我们可以使用会话来存储用户的设置,例如未经身份验证时。身份验证后会话用于识别已通过身份验证的用户。会话在用户身份验证和授权之间发挥着重要作用。

探索会议

传统上,会话是由服务器发送并存储在客户端的标识符。在下一个请求中,客户端会将会话令牌发送到服务器。服务器可以使用该标识符将请求与用户关联起来。

会话标识符可以存储在 Cookie、localStorage 和 sessionStorage 中。会话标识符可以通过 Cookie、URL 参数、隐藏表单字段或自定义标头发送回服务器。此外,服务器可以通过多种方式接受会话标识符。当网站和移动应用程序使用后端时,通常会出现这种情况。

会话标识符

会话标识符是存储在客户端的令牌。与会话标识符相关的数据位于服务器上。

一般来说,会话​​标识符:

  1. 必须是随机的;
  2. 应该存储在 cookie 中。

建议的会话 ID 长度必须为 128 位或 16 字节。建议使用优质的伪随机数生成器 (PNRG) 来生成熵,通常为 ID 长度的 50%。

Cookie 非常理想,因为它们会随每个请求一起发送,并且易于保护。LocalStorage 没有过期属性,因此可以持久保存。另一方面,SessionStorage 不会在多个选项卡/窗口之间持久保存,并且在关闭选项卡时会被清除。需要编写额外的客户端代码来处理 LocalStorage / SessionStorage。此外,两者都是 API,因此理论上它们容易受到XSS攻击。

通常,客户端和服务器之间的通信应通过 HTTPS 进行。会话标识符不应在协议之间共享。如果请求被重定向,则应刷新会话。此外,如果重定向到 HTTPS,则应在重定向后设置 Cookie。如果设置了多个 Cookie,后端应验证所有 Cookie。

保护 Cookie 属性

可以使用以下属性来保护 Cookie。

  • Secure属性指示浏览器仅通过 HTTPS 设置 Cookie。由于传输是通过 TLS 进行的,因此此属性可以防止中间人攻击 (MITM)。
  • HttpOnly属性阻止使用该对象的能力document.cookie。这可以防止 XSS 攻击窃取会话标识符。
  • SameSite属性阻止在跨域请求中发送 Cookie。这为CSRF 攻击提供了有限的保护。
  • 设置Domain&Path属性可以限制 Cookie 的暴露。默认情况下,Domain不应设置,并且Path应进行限制。
  • ExpireMax-Age允许我们设置 cookie 的持久性。

通常,会话库应该能够生成唯一的会话、刷新现有会话以及撤销会话。我们将express-session在下文中探索这个库。

使用 express-session 执行最佳实践

在使用 Express 的 Node.js 应用中,express-session是用于管理会话库。该库提供以下功能:

  • 基于 Cookie 的会话管理。
  • 用于管理会话存储的多个模块。
  • 用于生成、重新生成、销毁和更新会话的 API。
  • 安全 Cookie 的设置(安全 / HttpOnly / Expire / SameSite / Max Age / Expires /Domain / Path)

我们可以使用以下命令生成会话:

app.use(session({
  secret: 'veryimportantsecret',  
}))

Enter fullscreen mode Exit fullscreen mode

该密钥用于通过cookie-signature库对 Cookie 进行签名。Cookie 使用Hmac-sha256进行签名,并转换为base64字符串。我们可以将多个密钥作为数组。第一个密钥将用于对 Cookie 进行签名,其余密钥将用于验证。

app.use(session({
  secret: ['veryimportantsecret','notsoimportantsecret','highlyprobablysecret'],
}))

Enter fullscreen mode Exit fullscreen mode

要使用自定义会话 ID 生成器,我们可以使用genid参数。默认情况下,uid-safe用于生成字节长度为 24 的会话 ID。建议坚持使用默认实现,除非有特殊要求需要强化uuid

app.use(session({
    secret: 'veryimportantsecret', 
    genid: function(req) {
      return genuuid() // use UUIDs for session IDs
     }
}))
Enter fullscreen mode Exit fullscreen mode

Cookie 的默认名称是connect.sid。我们可以使用 来更改名称param。建议更改名称以避免被指纹识别。

app.use(session({
  secret: ['veryimportantsecret','notsoimportantsecret','highlyprobablysecret'], 
  name: "secretname" 
}))
Enter fullscreen mode Exit fullscreen mode

默认情况下,cookie 设置为

{ path: '/', httpOnly: true, secure: false, maxAge: null }
Enter fullscreen mode Exit fullscreen mode

为了强化我们的会话 cookie,我们可以指定以下选项:

app.use(session({
  secret: ['veryimportantsecret','notsoimportantsecret','highlyprobablysecret'],  
   name: "secretname",
  cookie: {
      httpOnly: true,
      secure: true,
      sameSite: true,
      maxAge: 600000 // Time is in miliseconds
  }
}))
Enter fullscreen mode Exit fullscreen mode

这里的注意事项是:

  • sameSite: true阻止基于 Cookie 的 CORS 请求。这将影响 API 调用和移动应用程序的工作流程。
  • secure需要 HTTPS 连接。此外,如果 Node 应用使用了代理(例如 Nginx),则需要将 proxy 设置为 true,如下所示。
app.set('trust proxy', 1)
Enter fullscreen mode Exit fullscreen mode

默认情况下,会话存储在 中MemoryStore。不建议在生产环境中使用。建议使用其他会话存储进行生产环境。我们提供多种数据存储选项,例如:

  • MySQL、MongoDB 等数据库。
  • 记忆存储类似Redis
  • ORM 库类似sequelize

我们将在这里使用 Redis 作为示例。

npm install redis connect-redis 
Enter fullscreen mode Exit fullscreen mode
const redis = require('redis');
const session = require('express-session');
let RedisStore = require('connect-redis')(session);
let redisClient = redis.createClient();

app.use(
  session({
    secret: ['veryimportantsecret','notsoimportantsecret','highlyprobablysecret'], 
     name: "secretname", 
     cookie: {
      httpOnly: true,
      secure: true,
      sameSite: true,
      maxAge: 600000 // Time is in miliseconds
  },
    store: new RedisStore({ client: redisClient ,ttl: 86400}),   
    resave: false
  })
)
Enter fullscreen mode Exit fullscreen mode

(生存时间ttl)参数用于设置过期日期。如果Expire在 Cookie 上设置了此属性,它将覆盖ttl。默认情况下,ttl有效期为一天。

我们还将其设置resave为 false。此参数强制将会话保存到会话存储中。请在查看存储文档后设置此参数。

session对象与所有路由相关联,并且可以在所有请求中访问。

router.get('/', function(req, res, next) {
  req.session.value = "somevalue";  
  res.render('index', { title: 'Express' });
});
Enter fullscreen mode Exit fullscreen mode

登录和权限提升后,应该重新生成会话。这可以防止会话固定攻击。要重新生成会话,我们将使用:

req.session.regenerate(function(err) {
  // will have a new session here
})
Enter fullscreen mode Exit fullscreen mode

当用户注销或超时时,会话应该过期。要销毁会话,我们可以使用:

req.session.destroy(function(err) {
  // cannot access session here
})
Enter fullscreen mode Exit fullscreen mode


附注:虽然本文重点介绍后端安全,但您也应该保护前端。请参阅这些关于保护ReactAngularVueReact NativeIonicNativeScript 的教程。

使用 Helmet.js (Cache-Control) 进行额外的安全保护

Web 缓存使我们能够更快地处理请求。一些敏感数据可能会缓存在客户端计算机上。即使会话超时,数据仍有可能从 Web 缓存中检索到。为了防止这种情况发生,我们需要禁用缓存。

从本文的角度来看,我们感兴趣的是设置Cache-Control标头以禁用客户端缓存。

Helmet.js 是一个 Express 库,可用于保护我们的 Express 应用程序。
该方法将为我们noCache设置Cache-ControlSurrogate-ControlPragma和HTTP 标头。Expires

const helmet = require('helmet')
app.use(helmet.noCache())
Enter fullscreen mode Exit fullscreen mode

不过,一般来说,使用其他选项也是明智之举。Helmet.js 提供:

  • dnsPrefetchControl控制浏览器 DNS 预取。
  • frameguard以防止点击劫持。
  • hidePoweredBy隐藏X-Powered-By标题。
  • hsts用于 HTTP 严格传输安全
  • noSniff防止客户端嗅探 MIME 类型
  • xssFilter添加一些 XSS 保护。

或者,如果网站有缓存要求,至少Cache-Control必须将标头设置为Cache-Control: no-cache="Set-Cookie, Set-Cookie2"

router.get('/', function(req, res, next) {
res.set('Cache-Control', "no-cache='Set-Cookie, Set-Cookie2'");
// Route Logic
})
Enter fullscreen mode Exit fullscreen mode

记录会话

每当创建、重新生成或销毁新会话时,都应记录日志。例如,用户角色升级或财务交易等活动都应记录日志。

典型的日志应包含时间戳、客户端 IP、请求的资源、用户 ID 和会话 ID。

这将有助于在发生攻击时检测会话异常。我们可以使用winstonmorganpino来记录这些请求。默认情况下,Express 已morgan预装。默认combined设置为我们提供了标准的 Apache 组合日志输出。

我们可以使用自定义 morgan 修改 morgan ,使其包含会话标识符tokens。根据用例,我们可以在输出中添加其他数据。其他日志库也可以实现类似的流程。

var express = require('express')
var morgan = require('morgan')

var app = express()

morgan.token('sessionid', function(req, res, param) {
    return req.sessionID;
});
morgan.token('user', function(req, res, param) {
    return req.session.user;
});

app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :user :sessionid'))

app.get('/', function (req, res) {
  res.send('hello, world!')
})
Enter fullscreen mode Exit fullscreen mode

根据用例,应该构建和实施日志记录场景。

额外的客户端防御

我们还可以采取一些其他客户端措施来使会话过期。

浏览器事件的会话超时

我们可以使用 JavaScript 来检测window.close事件是否被触发,然后强制会话注销。

超时警告

客户端可以通知用户会话超时。这将通知用户其会话即将过期。这在涉及较长的业务流程时非常有用。用户可以在超时前保存工作或继续工作。

初始登录超时

可以在页面加载和用户身份验证之间设置客户端超时时间。这是为了防止会话固定攻击,尤其是在用户使用公共/共享计算机时。

替代方案

目前,JWT是会话的可行替代方案。JWT 是一种无状态的身份验证机制。Bearer每个经过身份验证的请求的标头中都会发送一个令牌。JWT 令牌的有效负载包含用于授权的必要详细信息。当我们想要将部分数据公开为 API 资源时,这非常有用。然而,与会话不同,JWT 是无状态的,因此必须在客户端实现注销代码。您可以在 JWT 有效负载中设置到期时间戳,但不能强制注销。

最后的想法

正如我们在本教程中探讨的那样,在 Node/Express 应用程序中安全地管理会话是一项关键的安全要求。

我们重点介绍了一些技术来防止一些非常严重的攻击,如 CRSF、XSS 和其他可能泄露敏感用户信息的攻击。

在基于网络的攻击快速增长的时代,必须在开发应用程序时解决这些威胁,以最大限度地减少应用程序的攻击面。


要进一步了解 JavaScript 应用程序的安全性,请查看此数据表

鏂囩珷鏉ユ簮锛�https://dev.to/jscrambler/best-practices-for-secure-session-management-in-node-3f01
PREV
2020 年 React 中操作和使用组件的 9 种方法
NEXT
VScode 真的是开源的吗?