传统Web应用程序采用基于会话的认证方式,服务器需维护会话状态,存在以下问题:
每个用户会话都需要在服务器内存中占用空间,随着并发用户增加,服务器资源消耗迅速增长。大型应用可能需要维护数十万甚至数百万会话,严重影响服务器性能。
在分布式系统和集群环境下,会话状态难以共享。实现水平扩展时,需要采用会话复制、粘性会话或中央会话存储等机制,这些方案要么增加系统复杂度,要么引入单点故障风险,限制了系统的弹性扩展能力。
基于Cookie的会话受浏览器同源策略限制,跨域共享认证状态困难。在现代应用架构中,前后端分离、多端应用共存场景下,传统会话机制需要额外配置CORS、共享Cookie等复杂设置,增加了开发和维护成本。
传统会话认证依赖Cookie自动发送机制,容易遭受跨站请求伪造攻击。攻击者可以诱导用户访问恶意网站,利用浏览器自动附加的Cookie向受信任站点发起未授权请求,危及用户安全。防御CSRF需实现额外的令牌验证,增加了开发复杂度。
为解决上述问题,业界开始转向基于令牌的无状态认证机制,Web Token应运而生。
Web Token是一种紧凑、自包含的数据传输方式,可安全地在各方之间传递信息。这些信息经过数字签名,可验证其完整性和真实性。
Header.Payload.Signature
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "1234567890",
"name": "张三",
"iat": 1516239022,
"exp": 1516242622
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
const jwt = require('jsonwebtoken'); // 引入JWT库,用于生成和验证令牌
const secret = 'your-secret-key'; // 定义签名密钥,实际应用中应使用环境变量存储
// 生成token函数
function generateToken(user) {
return jwt.sign(
{ id: user.id, role: user.role }, // payload部分,包含用户ID和角色信息
secret, // 使用密钥进行签名
{ expiresIn: '2h' } // 设置令牌2小时后过期
);
}
// 验证token函数
function verifyToken(token) {
try {
return jwt.verify(token, secret); // 使用相同密钥验证令牌,成功则返回解码后的payload
} catch (err) {
return null; // 验证失败(令牌无效、过期或被篡改)返回null
}
}
// 存储token到浏览器本地存储
// 注意:在生产环境中考虑使用httpOnly cookie代替localStorage以增强安全性(指使用httpOnly Cookie存储JWT)
localStorage.setItem('token', receivedToken);
// API请求中使用token进行身份验证
fetch('https://api.example.com/data', {
headers: {
// 按照Bearer令牌规范添加到Authorization头
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
JWT黑名单是解决无状态令牌注销问题的技术方案。由于JWT设计为自包含且无状态,一旦签发就有效,直到过期,这导致传统注销机制失效。
// 使用Redis实现JWT黑名单
const redis = require('redis');
const client = redis.createClient();
// 将令牌加入黑名单,过期时间与令牌剩余有效期一致
async function blacklistToken(token) {
// 解码JWT获取过期时间(不验证签名)
const payload = jwt.decode(token);
if (!payload || !payload.exp) return false;
// 计算剩余有效期(秒)
const expiryTimeInSeconds = payload.exp - Math.floor(Date.now() / 1000);
if (expiryTimeInSeconds <= 0) return false; // 已过期,无需加入黑名单
// 以令牌的jti(JWT ID)或整个令牌作为键存入Redis
// 使用剩余有效期作为Redis键的过期时间,自动清理过期条目
await client.setEx(`bl_${payload.jti || token}`, expiryTimeInSeconds, '1');
return true;
}
// 检查令牌是否在黑名单中
async function isTokenBlacklisted(token) {
const payload = jwt.decode(token);
if (!payload) return true; // 无效令牌视为已黑名单
const blacklisted = await client.get(`bl_${payload.jti || token}`);
return !!blacklisted; // 存在返回true,不存在返回false
}
// 验证令牌(包含黑名单检查)
async function verifyToken(token) {
try {
// 先检查是否在黑名单中
const blacklisted = await isTokenBlacklisted(token);
if (blacklisted) return null;
// 不在黑名单中,进行正常JWT验证
return jwt.verify(token, secret);
} catch (err) {
return null;
}
}
// 分布式系统中使用Pub/Sub同步黑名单
function setupBlacklistSync() {
// 订阅黑名单更新频道
const subscriber = redis.createClient();
subscriber.subscribe('token_blacklist');
// 监听黑名单更新消息
subscriber.on('message', (channel, message) => {
if (channel === 'token_blacklist') {
// 可选:维护本地内存缓存以减少Redis查询
localBlacklistCache.add(message);
}
});
}
// 发布黑名单更新
async function blacklistAndPublish(token) {
await blacklistToken(token);
// 通知所有服务实例更新黑名单
const publisher = redis.createClient();
await publisher.publish('token_blacklist', token);
}
黑名单机制本质上为无状态JWT添加了有状态的检查层,在安全与性能间取得平衡,适用于对注销及令牌撤销有严格要求的系统。
无状态JWT完全依赖令牌本身,有状态JWT则需要服务端维护部分状态,各有利弊。
微服务环境下,需要考虑令牌传递策略,确保各服务可验证和获取必要信息。
随着分布式系统和零信任架构普及,Web Token技术将持续演进,与OIDC、FIDO2等标准进一步融合,提供更安全、便捷的身份认证机制。