身份验证:JWT

JWT(JSON Web Token)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象安全地传输信息。由于信息是经过数字签名的,因此可以被验证和信任。在 Web 应用中,JWT 常用于身份验证和授权,特别适用于分布式微服务、单页应用和移动端。

1. 什么是 JWT?

JWT 通常由三部分组成:Header(头部)Payload(负载)Signature(签名)。它们用点 . 连接成一个字符串:

xxxxx.yyyyy.zzzzz

其中各部分都是 Base64Url 编码的 JSON 字符串。

1.1 Header

头部通常包含两部分:令牌的类型(即 JWT)和使用的签名算法,例如 HMAC SHA256 或 RSA。

{
  "alg": "HS256",
  "typ": "JWT"
}

1.2 Payload

负载包含要传递的声明(claims)。声明可以是用户 ID、角色、过期时间等。有三类声明:

  • 注册声明:预定义的标准声明,如 iss(签发者)、exp(过期时间)、sub(主题)等,非强制但推荐。
  • 公共声明:自定义声明,但应避免与注册声明冲突。
  • 私有声明:双方约定的自定义声明。
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1735689600
}

1.3 Signature

签名用于验证消息在传输过程中未被篡改。它通过对头部和负载的编码字符串用指定算法和密钥进行签名生成。例如使用 HMAC SHA256:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

2. JWT 的工作流程

  1. 客户端(如浏览器、移动应用)向服务器发送登录请求(用户名/密码)。
  2. 服务器验证凭据,若成功则生成一个 JWT,并将其返回给客户端。
  3. 客户端存储 JWT(通常在 localStorage 或 httpOnly cookie 中)。
  4. 后续每次请求,客户端在 HTTP 头部 Authorization: Bearer <token> 中携带 JWT。
  5. 服务器接收到请求后,验证 JWT 的签名和有效性(如是否过期),解析出用户信息,然后处理请求。

这种方式是无状态的,服务器不需要存储会话信息,非常适合扩展。

3. 安装 jsonwebtoken

在 Node.js 中使用 JWT,最常用的库是 jsonwebtoken。安装:

npm install jsonwebtoken

同时,我们可能还需要 dotenv 管理密钥。

4. 生成 JWT

使用 jwt.sign() 方法生成 token。它接受 payload、密钥和可选选项(如过期时间)。

const jwt = require('jsonwebtoken');

const payload = { userId: 123, role: 'admin' };
const secret = process.env.JWT_SECRET || 'mysecret';
const token = jwt.sign(payload, secret, { expiresIn: '1h' });
console.log(token);

expiresIn 可以使用数值(秒)或描述字符串,如 '2 days''10h''7d'

5. 验证 JWT

使用 jwt.verify() 验证 token 的签名和有效性。如果验证通过,返回解码后的 payload;否则抛出错误。

try {
  const decoded = jwt.verify(token, secret);
  console.log(decoded); // { userId: 123, role: 'admin', iat: ..., exp: ... }
} catch (err) {
  console.error('Token 无效:', err.message);
}

注意:verify 会自动检查过期时间,如果 token 已过期,会抛出 TokenExpiredError

6. 在 Express 中实现登录和 token 颁发

创建一个简单的登录路由,验证用户凭据后返回 JWT。

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

app.use(express.json());

const secret = process.env.JWT_SECRET || 'mysecret';

// 模拟用户数据库
const users = [
  { id: 1, username: 'alice', password: 'password123' },
  { id: 2, username: 'bob', password: 'password456' }
];

app.post('/login', (req, res) => {
  const { username, password } = req.body;

  // 查找用户
  const user = users.find(u => u.username === username && u.password === password);
  if (!user) {
    return res.status(401).json({ error: '用户名或密码错误' });
  }

  // 生成 token
  const payload = { userId: user.id, username: user.username };
  const token = jwt.sign(payload, secret, { expiresIn: '1h' });

  res.json({ token });
});

app.listen(3000, () => console.log('Server running on port 3000'));

7. 创建验证中间件保护路由

中间件从请求头中提取 token,验证并挂载用户信息到 req.user,然后继续处理;如果验证失败,返回 401 或 403。

const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({ error: '未提供 token' });
  }

  jwt.verify(token, secret, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'token 无效或已过期' });
    }
    req.user = user;
    next();
  });
};

在需要保护的路由中使用该中间件:

app.get('/profile', authenticateToken, (req, res) => {
  res.json({ message: '这是个人资料', user: req.user });
});

8. 处理 token 过期与刷新机制

由于 access token 有效期较短(例如 15 分钟),为了良好的用户体验,通常引入 refresh token(刷新令牌)。refresh token 有效期较长(例如 7 天),存储在服务器端(如数据库),用于在 access token 过期后获取新的 access token。

简单流程:

  1. 登录时,除了返回 access token,还返回一个 refresh token(服务器存储其哈希,并返回给客户端)。
  2. 客户端存储 refresh token(通常放在 httpOnly cookie 中)。
  3. 当 access token 过期,客户端调用 /refresh-token 接口,携带 refresh token。
  4. 服务器验证 refresh token 的有效性(检查数据库),生成新的 access token 返回,并可选择轮换 refresh token。

示例 refresh token 接口:

// 假设已将 refresh token 存储到数据库
const refreshTokens = new Set(); // 简单示例,生产用数据库

app.post('/refresh-token', (req, res) => {
  const { refreshToken } = req.body;
  if (!refreshToken || !refreshTokens.has(refreshToken)) {
    return res.status(403).json({ error: '无效的 refresh token' });
  }

  jwt.verify(refreshToken, secret, (err, user) => {
    if (err) return res.status(403).json({ error: 'refresh token 无效' });

    // 生成新的 access token
    const newAccessToken = jwt.sign({ userId: user.userId }, secret, { expiresIn: '15m' });
    res.json({ accessToken: newAccessToken });
  });
});

注意:实际生产应考虑 refresh token 的存储安全性、轮换机制(每次刷新返回新 refresh token 并使旧失效)等。

9. 安全最佳实践

  • 使用强密钥:至少 32 个字符的随机字符串,通过环境变量注入。
  • 设置较短的 access token 过期时间(如 15 分钟),减少被盗用后的风险。
  • 使用 HTTPS 防止 token 在传输过程中被截获。
  • 考虑将 token 存储在 httpOnly cookie 中,可防止 XSS 攻击窃取 token,但需防范 CSRF。可以结合 CSRF token 或 SameSite 属性。
  • 不要在 payload 中存储敏感信息(如密码),因为 payload 只是 base64 编码,可被解码查看。
  • 实现 token 撤销机制(如黑名单),对于用户注销或密码变更等场景。
  • 对重要操作(如修改密码)要求二次验证。

10. 完整示例:用户认证系统

下面是一个包含登录、注册、受保护路由的完整 Express 应用,使用 JWT 进行认证。

require('dotenv').config();
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

app.use(express.json());

const secret = process.env.JWT_SECRET || 'mysecret';
const users = []; // 内存数据库

// 注册
app.post('/register', (req, res) => {
  const { username, password } = req.body;
  if (users.find(u => u.username === username)) {
    return res.status(400).json({ error: '用户已存在' });
  }
  const newUser = { id: users.length + 1, username, password };
  users.push(newUser);
  res.status(201).json({ message: '注册成功' });
});

// 登录
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username && u.password === password);
  if (!user) {
    return res.status(401).json({ error: '用户名或密码错误' });
  }
  const token = jwt.sign({ userId: user.id, username: user.username }, secret, { expiresIn: '1h' });
  res.json({ token });
});

// 验证中间件
const authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) return res.status(401).json({ error: '未提供 token' });
  const token = authHeader.split(' ')[1];
  jwt.verify(token, secret, (err, user) => {
    if (err) return res.status(403).json({ error: 'token 无效' });
    req.user = user;
    next();
  });
};

// 受保护路由
app.get('/profile', authenticate, (req, res) => {
  res.json({ message: '这是个人资料', user: req.user });
});

app.listen(3000, () => console.log('Server running on port 3000'));

11. 常见问题

  • JWT 如何注销? JWT 本身无状态,无法主动失效。解决方案:维护黑名单(将未过期的 token 加入黑名单),或使用短时效 token + refresh token 机制。
  • JWT 可以放在 URL 中吗? 不安全,因为 URL 会被记录在日志中,且容易泄露。应放在 Authorization 头或 httpOnly cookie 中。
  • 多个服务共享 secret? 微服务架构中,所有服务应使用相同的 secret 或公钥验证签名,或通过网关统一验证。
总结: JWT 是一种简单、自包含的认证机制,非常适合现代 Web 应用和微服务架构。通过本章学习,你应该掌握 JWT 的构成、生成、验证以及在 Express 中集成的方法,并了解安全注意事项。在实际项目中,务必结合业务需求选择合适的 token 策略。