•
阅读时长 2 分钟

Node.js 中的 Session 和 JWT:登录、过期与退出

目录

登录接口返回一个凭据,之后的请求带上它,服务端就可以识别用户。Session 和 JWT 都能完成这件事,但保存数据的位置和验证方式不同。

两种方式的区别,可以从过期、退出登录和权限更新这几个过程来看。

Session:通过 ID 查询服务端记录

Session 的常见实现,是把用户信息保存在服务端,客户端只保存一个随机 ID。每次请求,服务端通过这个 ID 查询用户信息。

下面的代码可以保存为 session.mjs,执行 node session.mjs:

import { randomBytes } from 'node:crypto';
import assert from 'node:assert/strict';

const sessions = new Map();

function createSession(user, now = Date.now()) {
  const id = randomBytes(24).toString('hex');
  sessions.set(id, { user, expires: now + 60_000 });
  return id;
}

function readSession(id, now = Date.now()) {
  const session = sessions.get(id);
  if (!session || session.expires <= now) {
    sessions.delete(id);
    return null;
  }
  return session.user;
}

const id = createSession('demo', 0);
assert.equal(readSession(id, 59_999), 'demo');
assert.equal(readSession(id, 60_000), null);

const anotherId = createSession('demo');
sessions.delete(anotherId);             // 退出,删除服务端记录
assert.equal(readSession(anotherId), null);
console.log('Session 过期和退出检查通过');

检查过期时传入固定时间,不需要真的等一分钟。退出时删除记录,之后即使请求仍然带着这个 ID,也查询不到用户了。

ID 通常通过 HttpOnly Cookie 保存和发送,完整请求过程可以看 Cookie 和 Session 登录示例。不过 Cookie 并不是 Session 唯一的传递方式。

服务端保存状态也带来一些工作:多个实例需要共享会话存储,过期记录需要清理,重启后是否保留登录状态也要确定。上面的 Map 只适合说明过程。

JWT:验证客户端带回的声明

JWT 通常由三部分组成:

header.payload.signature

header 说明签名算法等信息,payload 放用户 ID、过期时间等声明,signature 用来检查内容是否被修改。这里使用 HS256,服务端用同一个密钥签发和验证。

Base64URL 是编码,不是加密。 拿到 token 的人能够解出 payload,不要把密码等秘密放进去。常见的签名 JWT 保护的是内容完整性,不能让内容变得不可见。

下面代码保存为 jwt.mjs,执行 node jwt.mjs。手写实现用来观察签名和验证,实际认证服务应使用维护中的库,并配置完整的声明验证规则。

import { randomBytes, createHmac, timingSafeEqual } from 'node:crypto';
import assert from 'node:assert/strict';

const encode = value => Buffer.from(JSON.stringify(value)).toString('base64url');

function signToken(user, secret, now = Date.now()) {
  const header = encode({ alg: 'HS256', typ: 'JWT' });
  const payload = encode({ sub: user, exp: Math.floor(now / 1000) + 60 });
  const unsigned = header + '.' + payload;
  const signature = createHmac('sha256', secret)
    .update(unsigned).digest('base64url');
  return unsigned + '.' + signature;
}

function verifyToken(token, secret, now = Date.now()) {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) return null;
    const [header, payload, signature] = parts;
    const algorithm = JSON.parse(Buffer.from(header, 'base64url')).alg;
    if (algorithm !== 'HS256') return null;

    const expected = createHmac('sha256', secret)
      .update(header + '.' + payload).digest();
    const received = Buffer.from(signature, 'base64url');
    if (received.length !== expected.length ||
        !timingSafeEqual(received, expected)) return null;

    const data = JSON.parse(Buffer.from(payload, 'base64url'));
    if (typeof data.sub !== 'string' || !Number.isFinite(data.exp) ||
        data.exp <= Math.floor(now / 1000)) return null;
    return data.sub;
  } catch {
    return null;
  }
}

const secret = randomBytes(32);
const token = signToken('demo', secret, 0);
assert.equal(verifyToken(token, secret, 59_000), 'demo');
assert.equal(verifyToken(token, secret, 60_000), null);
assert.equal(verifyToken(token, randomBytes(32), 1), null);

const parts = token.split('.');
parts[1] = encode({ sub: 'admin', exp: 60 });
assert.equal(verifyToken(parts.join('.'), secret, 1), null);
console.log('JWT 过期、错误密钥和篡改检查通过');

验证时固定使用 HS256,不根据收到的 header 随意更换算法。签名正确以后,还要检查字段和过期时间。这个示例只检查 sub 和 exp;真实系统还要按用途验证 issuer、audience 等声明,考虑密钥轮换。

常见请求头写法如下,但 JWT 也可以放进 Cookie:

Authorization: Bearer <token>

Cookie 是浏览器保存和发送数据的机制,JWT 是令牌格式,两者不是互相排斥的登录方案。

退出后,旧凭据还能不能用

操作Session只检查签名和过期的 JWT
退出登录删除服务端记录,旧 ID 失效只删除客户端 token,其他副本仍然有效
凭据到期服务端检查 Session 的有效期服务端检查 exp
用户权限修改查询当前权限,或更新会话中的权限已签发 token 的权限声明不会自动更新

JWT 没有自动的“退出”动作。如果想提前撤销,需要维护撤销名单、令牌版本等状态,或者让关键操作重新查询服务端。这样就不能只靠验证签名完成所有检查了。

Session 也不是天然拥有最新权限。如果创建会话时复制了权限,后续只读会话中的旧数据,同样要考虑权限更新。

怎么选择

普通网站如果已经需要服务端管理会话,Session 的撤销过程比较直接。多个服务需要验证同一份声明时,JWT 可以减少对会话记录的查询,但有效期、撤销和密钥管理仍然要设计。

存储位置也需要单独考虑。放在 HttpOnly Cookie 中,要处理 CSRF;放在 JavaScript 可读取的位置,要考虑 XSS 导致 token 泄露。不能仅凭“用了 JWT”或“用了 Cookie”判断登录方案是否安全。

JWT 的结构和声明定义参考 RFC 7519。选择时更有用的是把退出、权限更新和跨服务验证的要求列出来,再看哪一种方案需要承担哪些工作。