在 Node.js 应用程序中使用 JWT 构建身份验证

在 Node.js 应用程序中使用 JWT 构建身份验证

🎤 你好,欢迎来到今天的讲座!

大家好!我是你们的讲师,今天我们要一起探讨如何在 Node.js 应用程序中使用 JSON Web Token(JWT)来构建强大的身份验证系统。如果你是第一次接触 JWT,或者你已经听说过它但还不太清楚它是怎么工作的,那么今天的内容绝对会让你受益匪浅。

我们将从基础开始,逐步深入到实际的代码实现,最后还会讨论一些常见的陷阱和最佳实践。别担心,我会尽量让这个过程轻松有趣,不会让你觉得枯燥无味。准备好了吗?那我们就开始吧!🚀


📝 什么是 JWT?

1. JWT 的定义

JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在网络应用之间安全地传输信息。JWT 是一种自包含的令牌,它可以携带用户的身份信息、权限信息等,并且可以通过数字签名来确保其完整性和真实性。

简单来说,JWT 就是一个经过编码和签名的字符串,通常由三部分组成:

  • Header(头部):描述了令牌的类型(通常是 JWT)以及所使用的签名算法(如 HS256RS256)。
  • Payload(载荷):包含了要传递的数据,例如用户的 ID、用户名、角色等。这些数据被称为“声明”(Claims),可以是标准声明(如 issexpsub 等),也可以是自定义声明。
  • Signature(签名):用于验证令牌的完整性和真实性。签名是通过对头部和载荷进行 Base64 编码后,使用指定的算法和密钥生成的。

2. JWT 的结构

一个典型的 JWT 看起来像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

这个字符串分为三个部分,用点号(.)分隔开:

  1. HeadereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  2. PayloadeyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
  3. SignatureSflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

每一部分都是经过 Base64 URL 编码的 JSON 对象。我们可以解码这些部分来看看里面的内容。

Header 解码

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

这里指定了签名算法为 HS256(HMAC SHA-256),并且表明这是一个 JWT。

Payload 解码

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

这是令牌的主体,包含了用户的 ID (sub)、名字 (name) 和令牌的签发时间 (iat)。

Signature 解码

签名部分并不是一个 JSON 对象,而是通过对头部和载荷进行编码后,使用密钥和算法生成的。它的作用是确保令牌没有被篡改。


🔑 为什么选择 JWT?

在现代的 Web 应用中,身份验证是非常重要的。传统的会话管理方式(如基于 Cookie 的会话)虽然有效,但在某些场景下可能会遇到问题。JWT 提供了一种更加灵活和安全的身份验证方式,尤其是在以下几种情况下:

1. 无状态的 API

JWT 是无状态的,这意味着服务器不需要存储任何与用户会话相关的信息。每次请求时,客户端都会将 JWT 作为授权凭证发送给服务器,服务器通过验证 JWT 来确认用户的身份。这种方式非常适合构建 RESTful API,尤其是当你的应用需要扩展到多个服务器或微服务架构时。

2. 跨域身份验证

由于 JWT 是基于令牌的,而不是依赖于服务器端的会话,因此它可以轻松地用于跨域请求。这对于单页应用(SPA)或移动应用非常有用,因为它们通常需要与多个域名或子域名进行通信。

3. 分布式系统

在分布式系统中,不同的服务可能运行在不同的服务器上。使用 JWT 可以避免每个服务都需要维护自己的会话管理机制。只要所有服务共享相同的密钥,它们就可以验证同一个 JWT。

4. 安全性

JWT 支持多种签名算法,包括对称加密(如 HS256)和非对称加密(如 RS256)。通过使用强加密算法和适当的密钥管理,JWT 可以提供较高的安全性。


🛠️ 如何在 Node.js 中使用 JWT?

现在我们已经了解了 JWT 的基本概念,接下来让我们看看如何在 Node.js 应用程序中使用 JWT 来实现身份验证。我们将使用一个流行的库 jsonwebtoken 来简化 JWT 的生成和验证过程。

1. 安装 jsonwebtoken

首先,我们需要安装 jsonwebtoken 库。你可以通过 npm 来安装它:

npm install jsonwebtoken

2. 生成 JWT

假设我们有一个用户登录的场景,用户输入用户名和密码后,服务器会验证这些凭据,并在验证成功后生成一个 JWT 令牌。以下是生成 JWT 的代码示例:

const jwt = require('jsonwebtoken');

// 假设我们有一个用户对象
const user = {
  id: 1,
  username: 'john_doe',
  role: 'admin'
};

// 定义一个密钥,用于签名 JWT
const secretKey = 'your_secret_key';

// 生成 JWT
const token = jwt.sign(user, secretKey, {
  expiresIn: '1h' // 令牌有效期为 1 小时
});

console.log('Generated JWT:', token);

在这个例子中,我们使用 jwt.sign() 方法生成了一个 JWT。该方法接受三个参数:

  1. payload:要包含在 JWT 中的数据(即用户信息)。
  2. secret:用于签名的密钥。
  3. options:可选参数,例如设置令牌的有效期(expiresIn)。

生成的 JWT 将包含用户的 ID、用户名和角色,并且会在 1 小时后过期。

3. 验证 JWT

当客户端发送带有 JWT 的请求时,服务器需要验证该令牌是否有效。我们可以通过 jwt.verify() 方法来完成这一操作。以下是一个简单的验证示例:

const jwt = require('jsonwebtoken');

// 假设我们接收到一个带有 JWT 的请求
const token = 'your_jwt_token_here';

// 定义一个密钥,用于验证 JWT
const secretKey = 'your_secret_key';

try {
  // 验证 JWT
  const decoded = jwt.verify(token, secretKey);

  console.log('Decoded JWT:', decoded);
} catch (err) {
  console.error('Invalid token:', err.message);
}

如果令牌有效,jwt.verify() 会返回解码后的用户信息;如果令牌无效或已过期,它会抛出一个错误。我们可以在中间件中使用这个逻辑来保护受限制的路由。

4. 使用中间件保护路由

为了简化路由保护的过程,我们可以编写一个中间件函数,该函数会在每个请求中自动验证 JWT。以下是一个示例:

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

// 定义一个密钥,用于验证 JWT
const secretKey = 'your_secret_key';

// 中间件:验证 JWT
function authenticateToken(req, res, next) {
  // 从请求头中获取 JWT
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.sendStatus(401); // 如果没有提供令牌,返回 401 未授权
  }

  try {
    // 验证 JWT
    const decoded = jwt.verify(token, secretKey);
    req.user = decoded; // 将解码后的用户信息附加到请求对象上
    next(); // 继续处理请求
  } catch (err) {
    return res.sendStatus(403); // 如果令牌无效,返回 403 禁止访问
  }
}

// 受保护的路由
app.get('/protected', authenticateToken, (req, res) => {
  res.json({ message: 'This is a protected route', user: req.user });
});

// 启动服务器
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

在这个例子中,我们创建了一个名为 authenticateToken 的中间件函数,它会在每个请求中检查是否存在有效的 JWT。如果令牌有效,它会将解码后的用户信息附加到 req.user 上,并继续处理请求;否则,它会返回 401 或 403 错误。


🕵️‍♂️ 处理 JWT 过期和刷新

JWT 的一个常见问题是它有时效性。一旦令牌过期,用户就需要重新登录。为了改善用户体验,我们可以引入“刷新令牌”机制。刷新令牌的作用是允许用户在不重新输入凭据的情况下,获取一个新的有效 JWT。

1. 生成刷新令牌

刷新令牌通常比访问令牌更长,甚至可以是永久性的。我们可以为每个用户生成一个唯一的刷新令牌,并将其存储在数据库中。以下是一个生成刷新令牌的示例:

const jwt = require('jsonwebtoken');

// 生成刷新令牌
function generateRefreshToken(userId) {
  return jwt.sign({ userId }, 'refresh_secret_key', { expiresIn: '7d' }); // 刷新令牌有效期为 7 天
}

// 存储刷新令牌(假设我们有一个数据库)
function storeRefreshToken(userId, token) {
  // 将刷新令牌存储在数据库中
  // 例如:db.refreshTokens.set(userId, token);
}

// 示例:生成并存储刷新令牌
const userId = 1;
const refreshToken = generateRefreshToken(userId);
storeRefreshToken(userId, refreshToken);

console.log('Generated refresh token:', refreshToken);

2. 刷新访问令牌

当用户的访问令牌过期时,他们可以使用刷新令牌来获取一个新的访问令牌。以下是一个处理刷新请求的示例:

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

// 模拟的数据库
const refreshTokens = new Map();

// 生成访问令牌
function generateAccessToken(user) {
  return jwt.sign(user, 'access_secret_key', { expiresIn: '15m' }); // 访问令牌有效期为 15 分钟
}

// 生成刷新令牌
function generateRefreshToken(userId) {
  return jwt.sign({ userId }, 'refresh_secret_key', { expiresIn: '7d' }); // 刷新令牌有效期为 7 天
}

// 存储刷新令牌
function storeRefreshToken(userId, token) {
  refreshTokens.set(userId, token);
}

// 获取新的访问令牌
app.post('/token', (req, res) => {
  const { refreshToken } = req.body;

  if (!refreshToken) {
    return res.sendStatus(401); // 如果没有提供刷新令牌,返回 401 未授权
  }

  if (!refreshTokens.has(refreshToken)) {
    return res.sendStatus(403); // 如果刷新令牌无效,返回 403 禁止访问
  }

  try {
    // 验证刷新令牌
    const decoded = jwt.verify(refreshToken, 'refresh_secret_key');
    const userId = decoded.userId;

    // 生成新的访问令牌
    const accessToken = generateAccessToken({ id: userId });

    res.json({ accessToken });
  } catch (err) {
    return res.sendStatus(403); // 如果刷新令牌无效,返回 403 禁止访问
  }
});

// 启动服务器
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

在这个例子中,我们创建了一个 /token 路由,用户可以通过 POST 请求发送他们的刷新令牌来获取一个新的访问令牌。服务器会验证刷新令牌是否有效,并在验证通过后生成一个新的访问令牌。

3. 保护刷新令牌

为了防止刷新令牌被滥用,我们应该采取一些额外的安全措施:

  • 限制刷新令牌的使用次数:每个刷新令牌只能使用一次,使用后应立即失效。
  • 设置较短的有效期:即使刷新令牌的有效期较长,我们也应该定期强制用户重新登录。
  • 使用 HTTPS:确保所有涉及令牌的通信都通过 HTTPS 进行,以防止中间人攻击。
  • 存储刷新令牌时加盐:在存储刷新令牌时,可以对令牌进行哈希处理,以增加安全性。

🛑 常见的 JWT 安全问题及解决方案

虽然 JWT 是一种强大的身份验证工具,但它也有一些潜在的安全问题。如果不正确使用,可能会导致严重的安全漏洞。下面我们来看看一些常见的问题及其解决方案。

1. 不要在 JWT 中存储敏感信息

JWT 是公开可读的,尽管它的签名可以防止篡改,但任何人都可以解码 JWT 并查看其中的内容。因此,我们不应该在 JWT 中存储任何敏感信息,如密码、信用卡号等。只应在 JWT 中存储必要的用户标识信息,如用户 ID 或角色。

2. 使用强密钥

JWT 的安全性依赖于签名密钥。如果攻击者能够获取到密钥,他们就可以伪造任意的 JWT。因此,我们必须确保使用足够强的密钥,并妥善保管它。建议使用至少 32 字节的随机密钥,并将其存储在环境变量中,而不是硬编码在代码中。

3. 设置合理的有效期

JWT 的有效期应该根据应用场景合理设置。对于短期使用的访问令牌,建议设置较短的有效期(如 15 分钟),以减少令牌泄露的风险。对于长期使用的刷新令牌,可以设置较长的有效期,但仍然需要定期强制用户重新登录。

4. 启用 HTTPS

所有涉及 JWT 的通信都应该通过 HTTPS 进行,以防止中间人攻击。HTTPS 可以确保令牌在传输过程中不会被窃取或篡改。

5. 防止重放攻击

重放攻击是指攻击者截获合法的 JWT,并在稍后的时间重复使用它。为了防止这种情况,我们可以为每个 JWT 添加一个唯一的时间戳或序列号,并在服务器端记录已使用的令牌。如果服务器检测到重复的令牌,可以拒绝该请求。


🎯 最佳实践总结

在本讲座中,我们学习了如何在 Node.js 应用程序中使用 JWT 来实现身份验证。为了确保系统的安全性和可靠性,以下是几点最佳实践的总结:

  1. 使用无状态的 JWT:JWT 是无状态的,服务器不需要存储会话信息,这使得它非常适合分布式系统和 RESTful API。
  2. 保护密钥:使用强密钥,并将其存储在安全的地方(如环境变量)。不要将密钥硬编码在代码中。
  3. 设置合理的有效期:根据应用场景合理设置 JWT 的有效期,避免令牌长时间有效带来的风险。
  4. 启用 HTTPS:确保所有涉及 JWT 的通信都通过 HTTPS 进行,以防止中间人攻击。
  5. 使用刷新令牌:引入刷新令牌机制,允许用户在不重新输入凭据的情况下获取新的访问令牌。
  6. 不要在 JWT 中存储敏感信息:JWT 是公开可读的,只应在其中存储必要的用户标识信息。

🎉 结语

恭喜你!你已经完成了今天的讲座,现在你应该对如何在 Node.js 应用程序中使用 JWT 构建身份验证有了更深入的理解。JWT 是一种强大而灵活的身份验证工具,只要我们遵循最佳实践,就能确保系统的安全性和可靠性。

如果你有任何问题或想法,欢迎在评论区留言。希望今天的讲座对你有所帮助,期待下次再见!✨


📝 附录:常用 JWT 标准声明

声明名称 描述
iss 发行人(Issuer)
sub 主题(Subject),通常是用户 ID 或用户名
aud 接收方(Audience)
exp 过期时间(Expiration Time),Unix 时间戳
nbf 生效时间(Not Before),Unix 时间戳
iat 签发时间(Issued At),Unix 时间戳
jti JWT ID,用于标识唯一的令牌

这些标准声明可以帮助我们在 JWT 中传递更多的元数据,从而更好地管理和验证令牌。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注