各位观众老爷们,晚上好!我是你们的老朋友,专门来给大家讲点儿干货的。今天咱们聊聊 Node.js 里面 JWT 这玩意儿,以及怎么让它乖乖地帮你搞定认证和授权,顺带解决 Token 刷新和撤销的问题。
准备好了吗?咱们这就开始了!
一、JWT 是个啥?为啥要用它?
想象一下,你开了一家豪华酒店,客人来入住,你得知道谁是VIP,谁是普通客人,谁是压根没预定的想混进来的。传统的做法是,每次客人想用酒店服务(比如点餐、用健身房),你都要查一下他/她的身份。这多麻烦!
JWT 就好比你给每个客人发了一张房卡,这张卡上写明了客人的身份信息、权限等等。客人拿着这张卡,就可以直接去享受酒店的服务,不需要每次都跑来前台验证身份。酒店的服务员(你的后端服务器)只需要验证一下这张卡是不是真的、有没有过期就行了。
所以,JWT 是一种基于 JSON 的开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为 JSON 对象。这个 JSON 对象可以被验证和信任,因为它是经过数字签名的。
简单来说,JWT 就是一个包含信息的、经过加密的字符串,用于身份验证和授权。
为啥要用 JWT 呢?
- 无状态 (Stateless): 服务器不需要保存 Session 信息,减轻服务器压力。
- 可扩展性 (Scalability): 由于无状态,更容易进行横向扩展。
- 跨域 (Cross-Domain): JWT 可以在不同的域之间传递。
- 安全性 (Security): 通过数字签名保证信息的完整性和不可篡改性。
二、JWT 的结构:三段论
JWT 由三个部分组成,每个部分用 .
分隔:
- Header (头部): 描述 JWT 的元数据,通常包含令牌类型 (JWT) 和所使用的签名算法 (例如:HMAC SHA256 或 RSA)。
- Payload (载荷): 包含声明 (claims)。声明是关于实体(通常是用户)和其他数据的声明。有三种类型的声明:
- Registered claims (注册声明): 一组预定义的声明,不是强制性的,但是推荐使用。包括:
iss
(issuer)、sub
(subject)、aud
(audience)、exp
(expiration time)、nbf
(not before)、iat
(issued at)、jti
(JWT ID)。 - Public claims (公共声明): 可以由 JWT 的使用者自定义,为了避免冲突,应该在 IANA JSON Web Token Registry 中注册,或者使用一个包含命名空间的 URI。
- Private claims (私有声明): 用于在应用程序之间共享信息。
- Registered claims (注册声明): 一组预定义的声明,不是强制性的,但是推荐使用。包括:
- Signature (签名): 为了验证消息的完整性,使用头部中指定的算法对头部、载荷和密钥进行签名。
举个栗子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- Header:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
=>{ "alg": "HS256", "typ": "JWT" }
(Base64 解码后) - Payload:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
=>{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
(Base64 解码后) - Signature:
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
三、Node.js 中 JWT 的使用:代码说话
咱们用 jsonwebtoken
这个库来玩转 JWT。
1. 安装:
npm install jsonwebtoken
2. 生成 JWT (Sign):
const jwt = require('jsonwebtoken');
// 定义用户数据
const user = {
id: 123,
username: 'testuser',
role: 'admin'
};
// 密钥 (务必保密!)
const secretKey = 'ThisIsMySuperSecretKey'; // 换成更复杂的!
// 生成 JWT
const token = jwt.sign(user, secretKey, {
expiresIn: '1h' // 设置过期时间为 1 小时
});
console.log('Generated Token:', token);
代码解释:
jwt.sign(payload, secretOrPrivateKey, [options])
:生成 JWT 的方法。payload
:要放入 JWT 中的数据。secretOrPrivateKey
:用于签名的密钥。生产环境务必使用强密码!options
:配置选项,例如过期时间 (expiresIn
)、算法 (algorithm
) 等。
expiresIn
:可以设置多种格式的时间,例如:'1h'
(1 小时),'30m'
(30 分钟),'7d'
(7 天),'1y'
(1 年)。
3. 验证 JWT (Verify):
const jwt = require('jsonwebtoken');
// 假设你从请求头中获取到 Token
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzLCJ1c2VybmFtZSI6InRlc3R1c2VyIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjc4ODgxODg1LCJleHAiOjE2Nzg4ODU0ODV9.X2b-nL9aMv_4n1kU9q17h8iLz3o7g9xQ2p9Yk8Z0g3A'; // 替换成实际的 Token
const secretKey = 'ThisIsMySuperSecretKey'; // 换成更复杂的!
// 验证 JWT
jwt.verify(token, secretKey, (err, decoded) => {
if (err) {
console.error('Token verification failed:', err.message);
// 处理验证失败的情况 (例如:Token 过期、无效签名)
} else {
console.log('Token is valid. Decoded payload:', decoded);
// 从 decoded 中获取用户信息
const userId = decoded.id;
const username = decoded.username;
const role = decoded.role;
console.log(`User ID: ${userId}, Username: ${username}, Role: ${role}`);
}
});
代码解释:
jwt.verify(token, secretOrPublicKey, [options], callback)
:验证 JWT 的方法。token
:要验证的 JWT 字符串。secretOrPublicKey
:用于验证签名的密钥。必须与生成 JWT 时使用的密钥相同。callback
:回调函数,用于处理验证结果。err
:如果验证失败,则包含错误信息。decoded
:如果验证成功,则包含解码后的 payload 数据。
4. 在 Express.js 中使用 JWT 进行身份验证:中间件来帮忙
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const port = 3000;
const secretKey = 'ThisIsMySuperSecretKey'; // 换成更复杂的!
// 身份验证中间件
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // 从 Authorization 头中获取 Token (Bearer <token>)
if (token == null) {
return res.sendStatus(401); // 未提供 Token
}
jwt.verify(token, secretKey, (err, user) => {
if (err) {
return res.sendStatus(403); // Token 无效
}
req.user = user; // 将用户信息添加到请求对象中
next(); // 继续执行下一个中间件或路由处理程序
});
};
// 模拟数据库中的用户
const users = [
{ id: 1, username: 'user1', password: 'password1' },
{ id: 2, username: 'user2', password: 'password2' }
];
// 登录路由
app.post('/login', (req, res) => {
// 模拟用户验证
const user = users.find(u => u.username === req.body.username && u.password === req.body.password);
if (user) {
// 生成 JWT
const token = jwt.sign({ id: user.id, username: user.username }, secretKey, { expiresIn: '1h' });
res.json({ token: token });
} else {
res.sendStatus(401); // 认证失败
}
});
// 需要身份验证的路由
app.get('/protected', authenticateToken, (req, res) => {
res.json({ message: 'Protected route accessed!', user: req.user }); // req.user 中包含用户信息
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
代码解释:
authenticateToken
中间件:- 从
Authorization
请求头中获取 Token。 - 如果 Token 不存在,返回 401 (Unauthorized)。
- 验证 Token 的有效性。
- 如果 Token 无效,返回 403 (Forbidden)。
- 如果 Token 有效,将解码后的用户信息添加到
req.user
中,并调用next()
继续执行。
- 从
/protected
路由:- 先经过
authenticateToken
中间件进行身份验证。 - 只有通过身份验证的用户才能访问该路由。
- 先经过
四、Token 刷新 (Refresh Token):让你的 Token 续命
Token 总有过期的时候,总不能让用户每次都重新登录吧?这时候就需要 Refresh Token 出马了。
思路:
- 用户登录成功后,服务器不仅返回 Access Token (用于访问受保护的资源),还返回一个 Refresh Token。
- Access Token 的过期时间设置得比较短 (例如:15 分钟)。
- 当 Access Token 过期后,客户端使用 Refresh Token 向服务器请求新的 Access Token。
- 服务器验证 Refresh Token 的有效性,如果有效,则生成新的 Access Token 和 Refresh Token (可选),并返回给客户端。
- Refresh Token 的过期时间可以设置得比较长 (例如:1 个月)。
代码示例:
const express = require('express');
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid'); // 用于生成 Refresh Token 的 ID
const app = express();
const port = 3000;
const secretKey = 'ThisIsMySuperSecretKey'; // 换成更复杂的!
const refreshTokenSecretKey = 'ThisIsMyRefreshTokenSecretKey'; // Refresh Token 的密钥也要单独设置!
// 模拟数据库存储 Refresh Token (生产环境需要使用数据库)
const refreshTokens = {};
// 登录路由
app.post('/login', (req, res) => {
// 模拟用户验证 (省略)
const user = { id: 1, username: 'testuser' };
// 生成 Access Token
const accessToken = jwt.sign({ id: user.id, username: user.username }, secretKey, { expiresIn: '15m' });
// 生成 Refresh Token
const refreshTokenId = uuidv4(); // 生成唯一的 Refresh Token ID
const refreshToken = jwt.sign({ id: user.id, username: user.username, tokenId: refreshTokenId }, refreshTokenSecretKey, { expiresIn: '1M' }); // 1 个月过期
// 存储 Refresh Token (生产环境存数据库)
refreshTokens[refreshTokenId] = { userId: user.id, token: refreshToken };
res.json({ accessToken: accessToken, refreshToken: refreshToken });
});
// 刷新 Token 路由
app.post('/refresh', (req, res) => {
const refreshToken = req.body.refreshToken;
if (!refreshToken) {
return res.sendStatus(401); // 未提供 Refresh Token
}
jwt.verify(refreshToken, refreshTokenSecretKey, (err, user) => {
if (err) {
console.error('Refresh token verification failed:', err.message);
return res.sendStatus(403); // Refresh Token 无效
}
const refreshTokenId = user.tokenId;
// 检查 Refresh Token 是否存在于数据库中
if (!refreshTokens[refreshTokenId] || refreshTokens[refreshTokenId].token !== refreshToken) {
console.log('Invalid refresh token: token not found or does not match.');
return res.sendStatus(403); // Refresh Token 不存在或不匹配
}
// 删除旧的 Refresh Token
delete refreshTokens[refreshTokenId];
// 生成新的 Access Token
const accessToken = jwt.sign({ id: user.id, username: user.username }, secretKey, { expiresIn: '15m' });
// 生成新的 Refresh Token (可选)
const newRefreshTokenId = uuidv4();
const newRefreshToken = jwt.sign({ id: user.id, username: user.username, tokenId: newRefreshTokenId }, refreshTokenSecretKey, { expiresIn: '1M' });
// 存储新的 Refresh Token (生产环境存数据库)
refreshTokens[newRefreshTokenId] = { userId: user.id, token: newRefreshToken };
res.json({ accessToken: accessToken, refreshToken: newRefreshToken });
});
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
代码解释:
/login
路由:生成 Access Token 和 Refresh Token,并将 Refresh Token 存储在内存中 (生产环境需要存储到数据库)。/refresh
路由:- 验证 Refresh Token 的有效性。
- 检查 Refresh Token 是否存在于数据库中。
- 删除旧的 Refresh Token。
- 生成新的 Access Token。
- 生成新的 Refresh Token (可选)。
- 存储新的 Refresh Token (生产环境需要存储到数据库)。
五、Token 撤销 (Token Revocation):让 Token 失效
有时候,我们需要让 Token 提前失效,比如用户主动注销、密码重置、或者发现 Token 泄露。
思路:
- 维护一个已撤销 Token 的黑名单 (Blacklist)。
- 每次验证 Token 时,先检查 Token 是否在黑名单中。如果在,则认为 Token 无效。
代码示例:
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const port = 3000;
const secretKey = 'ThisIsMySuperSecretKey'; // 换成更复杂的!
// 模拟数据库存储已撤销的 Token (生产环境需要使用数据库)
const revokedTokens = new Set();
// 身份验证中间件 (修改)
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) {
return res.sendStatus(401);
}
// 检查 Token 是否在黑名单中
if (revokedTokens.has(token)) {
console.log('Token has been revoked.');
return res.sendStatus(403); // Token 已被撤销
}
jwt.verify(token, secretKey, (err, user) => {
if (err) {
console.error('Token verification failed:', err.message);
return res.sendStatus(403); // Token 无效
}
req.user = user;
next();
});
};
// 登出路由 (撤销 Token)
app.post('/logout', authenticateToken, (req, res) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
// 将 Token 添加到黑名单
revokedTokens.add(token);
console.log('Token revoked:', token);
res.sendStatus(204); // No Content
});
// 需要身份验证的路由
app.get('/protected', authenticateToken, (req, res) => {
res.json({ message: 'Protected route accessed!', user: req.user });
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
代码解释:
revokedTokens
:使用Set
来存储已撤销的 Token。使用Set
可以高效地检查 Token 是否在黑名单中。authenticateToken
中间件:在验证 Token 之前,先检查 Token 是否在revokedTokens
中。/logout
路由:将 Token 添加到revokedTokens
中。
六、JWT 最佳实践:一些注意事项
- 使用强密钥: 密钥是 JWT 安全性的关键,务必使用足够长的、随机的、难以猜测的密钥。
- 保护密钥: 不要将密钥硬编码到代码中,也不要将密钥存储在版本控制系统中。可以使用环境变量、配置文件、或者专门的密钥管理系统来存储密钥。
- 设置合理的过期时间: Access Token 的过期时间应该设置得比较短,Refresh Token 的过期时间可以设置得比较长。
- 使用 HTTPS: 确保在传输 JWT 时使用 HTTPS,防止 Token 被窃取。
- 验证 Token 来源: 验证 Token 的
iss
(issuer) 和aud
(audience) 声明,确保 Token 来自可信的来源。 - 考虑使用 JWE: 如果需要在 JWT 中包含敏感信息,可以考虑使用 JWE (JSON Web Encryption) 对 JWT 进行加密。
- 监控和审计: 监控 JWT 的使用情况,并进行审计,及时发现和处理安全问题。
七、总结:JWT 的力量
JWT 提供了一种简单、安全、可扩展的方式来实现身份验证和授权。通过合理地使用 JWT,可以构建更加健壮和安全的 Web 应用程序。
- JWT 是一个标准: 遵循 RFC 7519 规范。
- JWT 是一个字符串: 由 Header、Payload 和 Signature 三部分组成。
- JWT 是一个令牌: 用于身份验证和授权。
- JWT 是一个解决方案: 用于解决无状态身份验证和授权的问题。
希望今天的讲座能帮助大家更好地理解和使用 JWT。记住,安全无小事,多注意细节,才能让你的应用更加安全可靠!
如果大家还有什么问题,欢迎提问,我会尽力解答。下次再见!