FeathersJS 中实现 JWT 令牌吊销机制详解
前言
在基于 JWT (JSON Web Token) 的身份验证系统中,令牌一旦签发就会在有效期内保持可用状态。本文将深入探讨如何在 FeathersJS 框架中实现 JWT 令牌的吊销机制,确保即使令牌未过期也能使其失效。
JWT 吊销的必要性
传统 Web 应用中,服务器可以通过销毁会话 ID 来注销用户。但在 JWT 的无状态认证中,由于服务器不存储令牌状态,要实现类似功能就需要额外机制。常见场景包括:
- 用户主动登出时使当前令牌失效
- 检测到可疑活动时强制终止会话
- 密码修改后使之前颁发的令牌失效
基础实现方案
核心思路
FeathersJS 的认证服务可通过扩展 AuthenticationService
类来实现令牌吊销功能。基本原理是:
- 维护一个已吊销令牌的存储
- 在验证令牌时检查是否已被吊销
- 登出时将令牌加入吊销列表
代码实现
const { AuthenticationService } = require('@feathersjs/authentication');
const { NotAuthenticated } = require('@feathersjs/errors');
// 存储已吊销的令牌
const revokedTokens = {};
class RevokableAuthService extends AuthenticationService {
async revokeAccessToken(accessToken) {
// 首先验证令牌是否有效
const verified = await this.verifyAccessToken(accessToken);
// 将令牌标记为已吊销
revokedTokens[accessToken] = true;
return verified;
}
async verifyAccessToken(accessToken) {
// 检查令牌是否已被吊销
if (revokedTokens[accessToken]) {
throw new NotAuthenticated('令牌已被吊销');
}
return super.verifyAccessToken(accessToken);
}
async remove(id, params) {
const authResult = await super.remove(id, params);
const { accessToken } = authResult;
if (accessToken) {
// 登出时吊销当前令牌
await this.revokeAccessToken(accessToken);
}
return authResult;
}
}
// 使用自定义认证服务
app.use('/authentication', new RevokableAuthService(app));
方案优缺点
优点:
- 实现简单,无需额外依赖
- 适合小型应用或开发环境
缺点:
- 内存存储,服务重启后吊销列表丢失
- 不适合分布式环境
- 长期运行可能导致内存增长
生产环境推荐方案:Redis 实现
对于生产环境,推荐使用 Redis 作为吊销令牌的存储后端,原因包括:
- 高性能的键值存储
- 支持自动过期(与 JWT 有效期天然契合)
- 适合分布式部署
Redis 实现代码
const redis = require('redis');
const { AuthenticationService } = require('@feathersjs/authentication');
const { NotAuthenticated } = require('@feathersjs/errors');
class RedisAuthService extends AuthenticationService {
constructor(app, configKey) {
super(app, configKey);
// 初始化 Redis 客户端
const client = redis.createClient();
this.redis = {
client,
get: client.get.bind(client),
set: client.set.bind(client),
exists: client.exists.bind(client),
expireat: client.expireat.bind(client)
};
// 连接 Redis
(async () => {
await this.redis.client.connect();
})();
}
async revokeAccessToken(accessToken) {
const verified = await this.verifyAccessToken(accessToken);
// 计算剩余有效时间(秒)
const expiry = verified.exp - Math.floor(Date.now() / 1000);
// 存储到 Redis 并设置自动过期
await this.redis.set(accessToken, '1', { EX: expiry });
return verified;
}
async verifyAccessToken(accessToken) {
if (await this.redis.exists(accessToken)) {
throw new NotAuthenticated('令牌已被吊销');
}
return super.verifyAccessToken(accessToken);
}
// 登出逻辑保持不变
async remove(id, params) {
const authResult = await super.remove(id, params);
const { accessToken } = authResult;
if (accessToken) {
await this.revokeAccessToken(accessToken);
}
return authResult;
}
}
app.use('/authentication', new RedisAuthService(app));
Redis 方案优势
- 自动清理:Redis 的过期机制会自动清理已过期的吊销记录
- 分布式友好:多个服务实例可以共享同一个 Redis 存储
- 高性能:Redis 的读取速度极快,对认证流程影响小
- 持久化:即使服务重启,吊销记录也不会丢失
实现细节解析
令牌验证流程
- 客户端携带 JWT 访问受保护资源
- 服务端首先检查 Redis 中是否存在该令牌
- 如果存在则拒绝访问(令牌已被吊销)
- 不存在则继续正常验证流程
吊销流程
- 用户登出或管理员主动吊销令牌
- 服务端验证令牌有效性
- 计算令牌剩余有效时间
- 将令牌存入 Redis 并设置相同过期时间
过期处理
利用 Redis 的自动过期特性,当 JWT 自然过期后,对应的吊销记录也会自动清除,避免存储无限增长。
最佳实践建议
- 监控 Redis 内存使用:确保有足够内存存储所有活跃令牌的吊销记录
- 设置适当的内存淘汰策略:如
volatile-lru
- 考虑集群部署:高并发场景下使用 Redis 集群提高可用性
- 记录吊销日志:便于审计和安全分析
- 实现批量吊销:支持按用户ID吊销所有相关令牌
扩展思考
- 黑名单与白名单:本方案实现的是黑名单模式,也可考虑实现白名单模式(只允许特定的未吊销令牌)
- 多因素认证集成:结合吊销机制实现更灵活的安全策略
- 令牌刷新机制:与刷新令牌流程协同工作
总结
在 FeathersJS 中实现 JWT 吊销机制是增强应用安全性的重要手段。对于生产环境,Redis 方案提供了高性能、可扩展的解决方案。开发者应根据实际业务需求选择合适的实现方式,并注意相关的最佳实践。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考