Node.js及Next.js无数据库场景下令牌存储的最佳实践
Node.js 访问令牌与刷新令牌存储最佳实践
通用Node.js场景的令牌存储最佳实践
访问令牌(Access Token)
- 客户端侧:
- Web应用:存在HttpOnly、Secure的Cookie中,开启SameSite属性(Strict/Lax),规避XSS和CSRF风险;绝对不能存在localStorage/sessionStorage里。
- 移动端:存在系统提供的安全密钥存储,比如iOS的Keychain、Android的Keystore,禁止明文存本地文件或SharedPreferences。
- 单页应用(SPA):仅存在内存中(比如React状态、Vue数据),页面刷新后重新获取,避免持久化带来的安全隐患。
- 服务端侧:
- 如果服务端需要持有access token(比如做API代理),优先存在Redis这类内存缓存中,设置与令牌过期时间一致的TTL;别存在进程内存(应用重启就丢失),更不能明文写入文件。
刷新令牌(Refresh Token)
- 客户端侧:
- 同access token,用HttpOnly、Secure Cookie存储,绑定用户设备标识,启用令牌轮换机制(每次刷新获取新的refresh token,旧的立即作废)。
- 服务端侧:
- 必须加密存储(比如AES加密),绑定用户ID和设备信息;
- 存放在持久化存储(数据库、Redis)中,设置较短的有效期(比如7-30天);
- 实现黑名单机制,用于主动撤销被盗或过期的令牌。
Next.js 无数据库的刷新令牌持久化方案
如果不想用传统数据库,且不能将令牌放在.env文件中(.env是静态配置,无法动态更新),可以参考以下几种方案:
方案1:加密文件存储
将加密后的刷新令牌存储在项目目录外的安全文件中,避免提交到版本控制,仅让服务端进程可读。
代码示例(Next.js API路由中实现):
// pages/api/refresh-token-handler.js const crypto = require('crypto'); const fs = require('fs').promises; const path = require('path'); // 从.env读取加密密钥(仅服务端可见) const ENCRYPTION_KEY = process.env.TOKEN_ENCRYPTION_KEY; // 需是32位字符串 const IV_LENGTH = 16; // 加密函数 function encrypt(text) { const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); let encrypted = cipher.update(text); encrypted = Buffer.concat([encrypted, cipher.final()]); return `${iv.toString('hex')}:${encrypted.toString('hex')}`; } // 解密函数 function decrypt(text) { const [ivHex, encryptedHex] = text.split(':'); const iv = Buffer.from(ivHex, 'hex'); const encryptedText = Buffer.from(encryptedHex, 'hex'); const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); let decrypted = decipher.update(encryptedText); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString(); } // 保存刷新令牌 export async function saveRefreshToken(token) { const encryptedToken = encrypt(token); // 存在项目外的安全目录,比如服务器的/var/secure目录 const tokenPath = path.join('/var/secure', 'nextjs-refresh-token.enc'); await fs.writeFile(tokenPath, encryptedToken); // 设置文件权限为仅所有者可读 await fs.chmod(tokenPath, 0o600); } // 读取刷新令牌 export async function getRefreshToken() { try { const tokenPath = path.join('/var/secure', 'nextjs-refresh-token.enc'); const encryptedToken = await fs.readFile(tokenPath, 'utf8'); return decrypt(encryptedToken); } catch (err) { // 文件不存在或读取失败时返回null return null; } }
注意:部署时要确保目标服务器存在指定的安全目录,且服务进程有读写权限。
方案2:Redis内存存储(轻量替代)
Redis属于内存键值存储,不算传统数据库,支持持久化(RDB/AOF),重启后数据不会丢失,适合需要动态更新令牌的场景。
代码示例:
// pages/api/refresh-token-handler.js import { createClient } from 'redis'; import crypto from 'crypto'; const ENCRYPTION_KEY = process.env.TOKEN_ENCRYPTION_KEY; const IV_LENGTH = 16; // 创建Redis客户端 const redisClient = createClient({ url: process.env.REDIS_URL }); redisClient.connect().catch(console.error); // 加密解密函数同方案1 // ... // 保存刷新令牌,设置30天有效期 export async function saveRefreshToken(token) { const encryptedToken = encrypt(token); await redisClient.set('app_refresh_token', encryptedToken, { EX: 30 * 24 * 60 * 60 }); } // 读取刷新令牌 export async function getRefreshToken() { const encryptedToken = await redisClient.get('app_refresh_token'); if (!encryptedToken) return null; return decrypt(encryptedToken); }
方案3:云服务商的参数存储服务
如果部署在云环境(比如Vercel、AWS),可以用云提供的参数存储服务,这类服务支持加密存储和动态更新,无需自己管理加密逻辑:
- Vercel Edge Config:可以在控制台直接更新令牌值,无需重新部署应用,代码示例:
// pages/api/refresh-token-handler.js import { get, set } from '@vercel/edge-config'; // 保存刷新令牌 export async function saveRefreshToken(token) { await set('app_refresh_token', token); } // 读取刷新令牌 export async function getRefreshToken() { return await get('app_refresh_token'); }
- AWS SSM Parameter Store:将令牌存在加密的参数中,通过AWS SDK读取和更新,适合AWS部署的场景。
内容的提问来源于stack exchange,提问作者ilyo
相关产品推荐
相关产品推荐

