在现代 Web 开发中,构建安全且高效的身份验证系统是我们面临的首要任务之一。你很可能已经熟悉 JWT (JSON Web Token) 这种无处不在的认证方式,它轻量、跨语言且易于集成。然而,在 2026 年的今天,随着 AI 辅助编码的普及和应用架构的日益复杂,单纯使用 JWT 往往面临安全性与用户体验的权衡:为了安全,Token 的有效期应该很短;但为了体验,我们又希望用户能保持长时间登录。
那么,如何解决这个问题呢?在这篇文章中,我们将深入探讨一种行业标准解决方案——JWT 结合刷新令牌。不同于传统的教程,我们将以 2026 年的视角,结合 AI 辅助开发 和 现代架构思维,教你如何在 Node.js 应用中从零实现这一机制,从而兼顾高安全性与流畅的用户体验。
核心概念:JWT 与刷新令牌
在动手写代码之前,让我们先夯实理论基础。理解这两种令牌的职责分工,是设计健壮系统的关键。就像我们在构建微服务架构时区分“热数据”和“冷数据”一样,Token 也有其不同的生命周期。
JWT (JSON Web Token) 的角色
JWT 本质上是一种紧凑且独立的方式,用于在各方之间安全地传输信息。在我们的身份验证上下文中,它通常作为访问令牌 使用。当用户登录后,服务器签发一个 JWT,客户端在后续的每次请求(比如访问受保护的 API INLINECODE12a26277 或 INLINECODE2f2ecea7)中都会带上这个 Token。
JWT 的主要优势在于它是无状态的。服务器不需要在数据库或 Session 中保存 Token 的副本,只需要验证签名即可。这意味着它非常适合横向扩展,无论是在 Kubernetes 集群还是 Serverless 边缘函数中,都能表现出色。
但是,这里有一个巨大的安全隐患。 如果我们签发的 JWT 有效期太长(例如 7 天),一旦黑客窃取了这个 Token(即使是通过 XSS 攻击),他们就可以冒充用户长达 7 天,而我们很难察觉。如果我们把有效期设置得很短(例如 15 分钟),用户可能刚喝了一口咖啡回来,就会发现由于 Token 过期,自己被强制退出了。这显然不是一个好的用户体验。
引入刷新令牌
为了解决上述矛盾,我们引入了“刷新令牌”。
刷新令牌的生命周期通常比访问令牌长得多(可能是 7 天、30 天甚至更久)。它的唯一职责,就是在旧的访问令牌过期后,用来向服务器申请一个新的访问令牌。
这种机制形成了一个双层防护体系:
- 访问令牌:短期有效(如 10-30 分钟)。即使泄露,由于生命周期极短,造成的损失也相对可控。它通常存储在内存中(如 React/Vue 的状态),随请求发送在 Header 中。
- 刷新令牌:长期有效(如 1-7 天)。它是获取新 Token 的“钥匙”。它通常存储在更加安全的地方,比如HttpOnly Cookie 中(这样可以有效防御 XSS 攻击读取 Cookie)。
实际应用场景:Auth Persistence(身份持久化)
让我们设想一个实际场景:用户正在浏览你的 Web 应用。他登录后,获得了一个有效期为 10 分钟的 Access Token 和一个有效期为 1 天的 Refresh Token。
当用户在页面间跳转时,我们使用 Access Token 来获取数据。一旦 10 分钟过去,Access Token 失效。此时,前端拦截到 401 未授权错误。我们不需要强行跳转到登录页,而是可以静默地在后台向服务器发送一个请求到 /refresh 路由,携带那个存放在 HttpOnly Cookie 中的 Refresh Token。服务器验证无误后,颁发一个新的 Access Token。用户对此毫无感知,仿佛一直处于登录状态。
这种机制让我们可以在无需任何用户凭据(不需要重新输入密码)的情况下,轻松地在页面刷新和登录会话之间维持用户状态。
2026 视角:架构设计与 AI 辅助开发
在我们最近的一个高性能金融科技项目中,我们重新审视了认证架构。在 2026 年,我们不仅关心代码的实现,更关心代码的可维护性和安全性。这就是为什么我们建议采用 JWT 结合 Redis 会话存储 的混合模式,并在开发过程中引入 AI 辅助编码 流程。
为什么建议在 Refresh Token 中引入 Redis?
传统的纯 JWT 方案有一个痛点:难以主动撤销。如果一个黑客盗取了 Refresh Token,在它过期前,服务器无法阻止它被使用。在 2026 年,随着 Redis 在边缘计算中的普及,我们建议将 Refresh Token 的哈希值存储在 Redis 中。这样,当我们需要“强制登出”用户时,只需从 Redis 中删除该键即可。这虽然引入了微弱的状态,但换来了极高的安全控制力。
利用 Cursor 与 GitHub Copilot 加速开发
作为现代开发者,我们现在是架构的监督者,而非盲目的打字员。在编写下面的代码之前,我们可以使用 Cursor IDE 或 Windsurf,通过 Prompt(提示词)直接生成基础脚手架。
你可以在 AI IDE 中尝试这样的提示词:
> "创建一个 Node.js Express 项目结构,包含 dotenv, cookie-parser, jsonwebtoken 和 ioredis。实现一个登录接口,返回 Access Token 和 HttpOnly Cookie。请确保代码遵循 ESLint Standard 规范,并包含详细的错误处理。"
通过这种方式,我们可以将精力集中在业务逻辑和安全性审查上,而不是重复的样板代码。让我们来看看具体的核心实现。
环境搭建与核心逻辑实现
步骤 1:项目初始化
首先,确保你的机器上已经安装了 Node.js 和 npm。打开终端,创建一个新的项目文件夹并初始化它。
npm init -y
步骤 2:安装依赖包
我们需要安装几个核心包。注意,为了应对 2026 年的高并发需求,我们额外引入了 ioredis 用于高性能缓存管理。
npm install express cookie-parser dotenv jsonwebtoken ioredis
- express: 我们的 Web 框架。
- cookie-parser: 用于解析发送到服务器的 Cookie 中的 Refresh Token。
- dotenv: 用于管理敏感信息(如密钥),不要把它们硬编码在代码里。
- jsonwebtoken: 用于生成和验证 Token。
- ioredis: Redis 客户端,用于存储 Refresh Token 白名单(生产级实践)。
步骤 3:配置环境变量
在项目根目录下创建一个名为 .env 的文件。我们需要两个密钥和一个 Redis 连接字符串。
ACCESS_TOKEN_SECRET=your_super_secret_access_key_here_change_this
REFRESH_TOKEN_SECRET=your_super_secret_refresh_key_here_change_this
REDIS_URI=redis://localhost:6379
生产级代码实现
1. 构建 Express 服务器与中间件
现在,让我们编写 INLINECODE2cdbf529。我们将实现两个核心路由:INLINECODEf63ba6e8(颁发令牌)和 /refresh(刷新令牌)。为了生产环境的健壮性,我们引入了 Redis 来管理 Refresh Token 的白名单。
const dotenv = require(‘dotenv‘);
const express = require(‘express‘);
const cookieParser = require(‘cookie-parser‘);
const jwt = require(‘jsonwebtoken‘);
const Redis = require(‘ioredis‘);
// 加载环境变量
dotenv.config();
const app = express();
// 初始化 Redis 客户端(用于存储 Refresh Token 的白名单)
// 在生产环境中,建议使用集群模式或连接池
const redis = new Redis(process.env.REDIS_URI);
// 配置中间件
app.use(express.json());
app.use(cookieParser());
// 模拟用户数据库
const userCredentials = {
username: ‘admin‘,
password: ‘admin123‘, // 实际应用中请存储密码的哈希值!
email: ‘[email protected]‘,
id: ‘user_123‘ // 唯一标识符
};
2. 实现 Login 路由(含 Redis 存储与 Token 轮换逻辑)
登录逻辑的核心不仅仅是签发 Token,还要建立服务端的信任链。我们将生成的 Refresh Token 存入 Redis,以便后续验证和撤销。
app.post(‘/login‘, async (req, res) => {
const { username, password } = req.body;
// 1. 校验凭据(实际项目中请使用 bcrypt 对比哈希值)
if (username === userCredentials.username && password === userCredentials.password) {
// 2. 生成 Access Token (有效期短,例如 10 分钟)
// Payload 包含了必要的业务信息,避免每次都查库
const accessToken = jwt.sign({
userId: userCredentials.id,
username: userCredentials.username,
email: userCredentials.email
}, process.env.ACCESS_TOKEN_SECRET, { expiresIn: ‘10m‘ });
// 3. 生成 Refresh Token (有效期长,例如 1 天)
const refreshToken = jwt.sign({
userId: userCredentials.id
}, process.env.REFRESH_TOKEN_SECRET, { expiresIn: ‘1d‘ });
// 4. 【关键步骤】将 Refresh Token 存入 Redis 白名单
// Key 设计: refresh_token:{userId}, Value: Token字符串
// 这样我们可以在后端强制让 Token 失效
await redis.set(`refresh_token:${userCredentials.id}`, refreshToken, ‘EX‘, 24 * 60 * 60);
// 5. 将 Refresh Token 设置为 HttpOnly Cookie
// secure: true (生产环境必须开启 HTTPS)
// sameSite: ‘Strict‘ (防止 CSRF 攻击)
res.cookie(‘jwt‘, refreshToken, {
httpOnly: true,
sameSite: ‘Strict‘,
secure: process.env.NODE_ENV === ‘production‘, // 自动适配环境
maxAge: 24 * 60 * 60 * 1000
});
return res.json({ accessToken });
}
else {
return res.status(401).json({ message: ‘用户名或密码错误‘ });
}
});
3. 实现 Refresh 路由与 Token 轮换
这里是 2026 年标准的刷新策略:Token 轮换。每次刷新时,我们不仅颁发新的 Access Token,还会更换新的 Refresh Token,并使旧的立即失效。这能有效防止“重放攻击”。
app.post(‘/refresh‘, async (req, res) => {
// 1. 检查 Cookie
if (req.cookies?.jwt) {
const oldRefreshToken = req.cookies.jwt;
// 2. 验证 Token 签名
jwt.verify(oldRefreshToken, process.env.REFRESH_TOKEN_SECRET, async (err, decoded) => {
if (err) {
// Token 篡改或过期,清除 Cookie
res.clearCookie(‘jwt‘);
return res.status(403).json({ message: ‘Refresh Token 无效‘ });
}
const userId = decoded.userId;
// 3. 【关键步骤】校验 Redis 白名单
// 获取服务器存储的 Token
const storedToken = await redis.get(`refresh_token:${userId}`);
if (storedToken !== oldRefreshToken) {
// 如果不一致,说明该 Token 已被使用过(被盗用的迹象)或已被撤销
// 拒绝刷新,并清除 Cookie
res.clearCookie(‘jwt‘);
return res.status(403).json({ message: ‘Token 篡改或已失效,请重新登录‘ });
}
// 4. 验证通过,执行 Token 轮换
// 签发新的一对 Token
const newAccessToken = jwt.sign({
userId: userId,
username: userCredentials.username // 简化处理,实际应查库
}, process.env.ACCESS_TOKEN_SECRET, { expiresIn: ‘10m‘ });
const newRefreshToken = jwt.sign({
userId: userId
}, process.env.REFRESH_TOKEN_SECRET, { expiresIn: ‘1d‘ });
// 5. 更新 Redis 白名单(替换旧 Token)
await redis.set(`refresh_token:${userId}`, newRefreshToken, ‘EX‘, 24 * 60 * 60);
// 6. 更新客户端 Cookie
res.cookie(‘jwt‘, newRefreshToken, {
httpOnly: true,
sameSite: ‘Strict‘,
secure: process.env.NODE_ENV === ‘production‘,
maxAge: 24 * 60 * 60 * 1000
});
return res.json({ accessToken: newAccessToken });
});
} else {
return res.status(401).json({ message: ‘未找到 Refresh Token‘ });
}
});
// 启动服务器
const PORT = 4000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
4. 保护路由与错误处理中间件
最后,我们需要一个中间件来保护 API 资源。在我们的项目中,我们通常会将此逻辑抽象为全局中间件或装饰器,但为了演示清晰,我们在这里直接实现。
const authenticateToken = (req, res, next) => {
const authHeader = req.headers[‘authorization‘];
const token = authHeader && authHeader.split(‘ ‘)[1];
if (!token) return res.status(401).json({ message: ‘缺少 Access Token‘ });
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return res.status(403).json({ message: ‘Access Token 无效或过期‘ });
req.user = user;
next();
});
};
// 受保护的路由
app.get(‘/protected‘, authenticateToken, (req, res) => {
res.json({ message: ‘这是敏感数据‘, user: req.user });
});
2026 年最佳实践与常见陷阱
通过上述代码,我们已经构建了一个不仅安全,而且具备可观测性的认证系统。但在实施过程中,我们总结了几个必须要避免的陷阱,这些都是在与 AI 协作调试过程中积累的经验。
1. 警惕 XSS 与 Token 存储位置
在早期开发中,很多开发者为了方便,将 Access Token 也存放在 LocalStorage 中。在 2026 年,这被视为巨大的安全隐患。LocalStorage 容易受到 XSS 攻击。
我们的最佳实践是:
- Access Token: 存储在内存中(如 React State、Pinia Store)。
- Refresh Token: 必须存储在 HttpOnly Cookie 中。
这样,即使前端代码存在 XSS 漏洞,黑客也无法窃取 Refresh Token,从而无法伪造新的 Access Token。
2. 处理并发请求的 Token 过期问题
在实际场景中,你可能会遇到这样的情况:页面加载时同时发出了 5 个 API 请求,而此时 Access Token 刚好过期。这会导致 5 个 /refresh 请求同时发往服务器。
解决策略:前端需要实现“请求锁定”机制。当检测到 401 错误时,将后续的请求挂起,只发起一个刷新请求。刷新成功后,统一使用新 Token 重试所有挂起的请求。这虽然增加了前端的复杂度,但对于现代应用来说是必须的。
3. 侧向移动与 CSRF 防御
使用 HttpOnly Cookie 固然好,但它引入了 CSRF(跨站请求伪造)的风险。为了防御这一点,我们使用了 SameSite: ‘Strict‘ 属性。此外,在 2026 年,我们通常会配合 Double Submit Cookie 模式或 CSRF Token 来双重加固。
4. 不要在 JWT Payload 中存储敏感数据
JWT 只是 Base64 编码,任何人都可以解码。绝对不要在 Payload 中放密码、信用卡号等隐私信息。只放 User ID、Role 等非敏感标识符。
总结
通过这篇文章,我们从零开始构建了一个符合 2026 年标准的身份验证系统。我们不仅使用了 JWT 和 Refresh Token,还结合 Redis 实现了服务端的“黑名单/白名单”控制,从而在无状态和安全性之间找到了完美的平衡点。
当我们再次审视这段代码时,我们可以骄傲地说:这套系统不仅保护了用户的数据安全,还利用了 Token 轮换机制防御了高级攻击。最重要的是,通过引入现代化的开发工具,我们能够以极快的速度构建出如此健壮的系统。希望这些经验能帮助你在下一个全栈项目中游刃有余!