MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js身份验证JWT实现详解

2021-11-251.4k 阅读

什么是JWT

JSON Web Token(JWT)是一种用于在网络应用环境间安全传递信息的开放标准(RFC 7519)。它定义了一种紧凑且自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。JWT通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。

JWT的结构

  1. 头部(Header) 头部通常由两部分组成:令牌的类型(即JWT),以及使用的哈希算法,如HMAC SHA256或RSA。例如:
{
  "alg": "HS256",
  "typ": "JWT"
}

然后将这个JSON对象使用Base64Url编码,形成JWT的第一部分。

  1. 载荷(Payload) 载荷是JWT的第二部分,它是一个包含声明(claims)的JSON对象。声明是关于实体(通常是用户)和其他数据的陈述。有三种类型的声明:注册声明(如iss、exp、sub等,这些是预定义的,可选但推荐使用)、公共声明(可以由使用JWT的各方定义)和私有声明(用于在同意使用它们的各方之间共享信息)。例如:
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

同样,将这个JSON对象使用Base64Url编码,形成JWT的第二部分。

  1. 签名(Signature) 要创建签名部分,需要使用编码后的头部、编码后的载荷、一个密钥(secret),以及头部中指定的签名算法。例如,如果使用HMAC SHA256算法,签名将按如下方式创建:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

签名用于验证消息在传输过程中没有被更改,并且在使用私钥签名的情况下,还可以验证JWT的发送者的身份。

在Node.js中使用JWT进行身份验证的优势

  1. 无状态性:JWT使得服务器可以保持无状态。服务器不需要在内存中存储关于用户会话的任何信息,每次请求都包含了足够的验证信息。这使得应用程序更容易扩展,因为服务器不需要在多个实例之间共享会话状态。
  2. 跨域友好:由于JWT是通过HTTP头或URL参数传递的,它可以很方便地在不同的域之间传递,适用于单页应用(SPA)、移动应用等跨域场景。
  3. 自包含:JWT包含了用户身份验证和授权所需的所有信息,服务器在验证签名后可以直接从JWT中获取这些信息,而不需要额外的数据库查询。

Node.js中实现JWT身份验证的步骤

  1. 安装依赖 在Node.js项目中,我们可以使用jsonwebtoken库来处理JWT。首先,确保你已经初始化了一个package.json文件(使用npm init -y命令),然后安装jsonwebtoken
npm install jsonwebtoken
  1. 生成JWT 在用户登录成功后,我们需要生成一个JWT并返回给客户端。以下是一个简单的Node.js示例,展示如何生成JWT:
const jwt = require('jsonwebtoken');
const secretKey = 'your-secret-key';

// 假设用户登录成功,获取到用户信息
const user = {
  id: 1,
  username: 'testuser',
  email: 'test@example.com'
};

// 生成JWT
const token = jwt.sign({ user }, secretKey, { expiresIn: '1h' });
console.log(token);

在上述代码中,jwt.sign方法接受三个参数:

  • 第一个参数是载荷对象,这里我们将用户信息包含在其中。
  • 第二个参数是用于签名的密钥,这个密钥应该保密,不能泄露在客户端代码中。
  • 第三个参数是一个选项对象,expiresIn指定了JWT的过期时间,这里设置为1小时。
  1. 验证JWT 在后续的受保护路由中,我们需要验证客户端发送的JWT。以下是一个Express.js应用中验证JWT的示例:
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const secretKey = 'your-secret-key';

// 模拟一个受保护的路由
app.get('/protected', (req, res) => {
  const token = req.headers['authorization'];

  if (!token) {
    return res.status(401).json({ message: 'Access token is missing' });
  }

  const bearerToken = token.split(' ')[1];

  jwt.verify(bearerToken, secretKey, (err, decoded) => {
    if (err) {
      return res.status(403).json({ message: 'Invalid token' });
    }

    res.json({ message: 'This is a protected route', user: decoded.user });
  });
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

在上述代码中:

  • 首先,我们从请求头的authorization字段中获取JWT。如果没有找到authorization头,返回401状态码,表示未授权。
  • 然后,我们提取bearer令牌(去除Bearer 前缀)。
  • 接着,使用jwt.verify方法验证JWT。如果验证失败(例如,令牌过期、签名无效等),err会被设置,我们返回403状态码,表示禁止访问。如果验证成功,decoded将包含解码后的载荷信息,我们可以从中获取用户信息并返回受保护的内容。

JWT的安全性考虑

  1. 密钥管理:用于签名JWT的密钥必须严格保密。密钥泄露将导致任何人都可以伪造有效的JWT,从而获得未授权的访问。密钥应该足够复杂,并且定期更换。
  2. 过期时间设置:合理设置JWT的过期时间很重要。如果过期时间设置过长,可能会增加安全风险,因为即使在用户会话应该结束的情况下,令牌仍然有效。如果过期时间设置过短,可能会给用户带来不便,导致频繁的重新登录。
  3. 防止重放攻击:虽然JWT本身没有内置的防止重放攻击的机制,但可以通过在JWT中添加唯一标识符(如jti声明),并在服务器端维护一个已使用的JWT列表来防止重放攻击。每次验证JWT时,检查其jti是否已在使用列表中。
  4. 传输安全:JWT应该通过安全的通道(如HTTPS)传输,以防止中间人攻击。在传输过程中,JWT可能包含敏感信息,如果被拦截,攻击者可能会使用这些信息进行恶意操作。

基于角色的授权与JWT

除了身份验证,JWT还可以用于基于角色的授权。我们可以在JWT的载荷中添加用户的角色信息,然后在服务器端根据角色来决定用户是否有权限访问特定的资源。

在JWT载荷中添加角色信息

在生成JWT时,将用户角色添加到载荷中:

const jwt = require('jsonwebtoken');
const secretKey = 'your-secret-key';

const user = {
  id: 1,
  username: 'testuser',
  email: 'test@example.com',
  role: 'admin'
};

const token = jwt.sign({ user }, secretKey, { expiresIn: '1h' });
console.log(token);

根据角色进行授权

在Express.js应用中,我们可以在受保护路由中检查用户角色:

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const secretKey = 'your-secret-key';

// 模拟一个只有管理员能访问的路由
app.get('/admin-only', (req, res) => {
  const token = req.headers['authorization'];

  if (!token) {
    return res.status(401).json({ message: 'Access token is missing' });
  }

  const bearerToken = token.split(' ')[1];

  jwt.verify(bearerToken, secretKey, (err, decoded) => {
    if (err) {
      return res.status(403).json({ message: 'Invalid token' });
    }

    if (decoded.user.role!== 'admin') {
      return res.status(403).json({ message: 'Access denied. Only admins can access this route' });
    }

    res.json({ message: 'This is an admin - only route' });
  });
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

通过这种方式,我们可以很方便地根据用户角色来控制对不同资源的访问。

处理JWT刷新令牌

由于JWT通常设置了较短的过期时间以提高安全性,用户可能会频繁遇到令牌过期的情况。为了解决这个问题,我们可以使用刷新令牌(Refresh Token)。

什么是刷新令牌

刷新令牌是一个长期有效的令牌,用于在访问令牌(JWT)过期时获取新的访问令牌。刷新令牌通常存储在客户端的安全位置(如HTTP-only cookie),以防止XSS攻击。

实现刷新令牌机制

  1. 生成刷新令牌 在用户登录时,除了生成JWT,还生成一个刷新令牌。我们可以使用crypto模块在Node.js中生成一个随机的刷新令牌:
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const secretKey = 'your-secret-key';

const user = {
  id: 1,
  username: 'testuser',
  email: 'test@example.com'
};

// 生成JWT
const accessToken = jwt.sign({ user }, secretKey, { expiresIn: '15m' });

// 生成刷新令牌
const refreshToken = crypto.randomBytes(64).toString('hex');

console.log('Access Token:', accessToken);
console.log('Refresh Token:', refreshToken);
  1. 存储刷新令牌 刷新令牌应该存储在服务器端的数据库中,与用户关联。这里我们简单模拟一个存储在内存中的情况:
const refreshTokens = [];

// 假设用户登录成功,将刷新令牌与用户关联存储
const user = { id: 1 };
const refreshToken = 'generated - refresh - token';
refreshTokens.push({ userId: user.id, token: refreshToken });
  1. 使用刷新令牌获取新的访问令牌 创建一个API端点,用于接收刷新令牌并返回新的访问令牌:
const express = require('express');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const app = express();
const secretKey = 'your-secret-key';

// 模拟存储刷新令牌的数组
const refreshTokens = [];

// 刷新令牌端点
app.post('/refresh', (req, res) => {
  const { refreshToken } = req.body;

  const storedToken = refreshTokens.find(token => token.token === refreshToken);

  if (!storedToken) {
    return res.status(403).json({ message: 'Invalid refresh token' });
  }

  const user = { id: storedToken.userId };
  const newAccessToken = jwt.sign({ user }, secretKey, { expiresIn: '15m' });

  res.json({ accessToken: newAccessToken });
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

在实际应用中,刷新令牌应该存储在数据库(如MongoDB、MySQL等)中,并且需要考虑安全性,如对刷新令牌进行加密存储等。

JWT在不同场景下的应用

  1. 单页应用(SPA):在SPA中,JWT可以在用户登录后存储在客户端(如localStoragesessionStorage),每次请求时将JWT添加到请求头中。这样,服务器可以验证JWT并提供相应的服务,而不需要在服务器端维护会话状态。
  2. 移动应用:移动应用同样可以使用JWT进行身份验证和授权。JWT可以存储在移动设备的本地存储中,在每次API请求时发送。由于移动应用通常需要与多个后端服务交互,JWT的无状态性和自包含性使其非常适合这种场景。
  3. 微服务架构:在微服务架构中,各个微服务之间可能需要进行身份验证和授权。JWT可以在服务之间传递,每个微服务可以独立验证JWT的有效性,而不需要依赖中央认证服务来验证用户身份。

总结Node.js中JWT身份验证的最佳实践

  1. 始终使用HTTPS:确保在传输JWT时使用HTTPS,以防止令牌被拦截和篡改。
  2. 合理设置过期时间:根据应用的安全需求和用户体验,合理设置JWT的过期时间。同时,结合刷新令牌机制,确保用户在令牌过期时能够方便地获取新的令牌。
  3. 保护密钥:用于签名JWT的密钥是整个身份验证机制的关键。密钥应该存储在安全的地方,并且定期更换。
  4. 验证输入:在验证JWT之前,确保对输入进行充分的验证,如检查authorization头是否存在、格式是否正确等。
  5. 日志记录:记录与JWT验证相关的事件,如成功验证、无效令牌等,以便于排查问题和审计。

通过遵循这些最佳实践,可以在Node.js应用中构建一个安全、可靠的JWT身份验证系统,为用户提供安全的访问体验,同时保护应用的资源不被未授权访问。在实际开发中,还需要根据具体的业务需求和安全要求,对JWT的使用进行适当的调整和优化。