JWT + HttpOnly Cookie 的登录鉴权方案 | 拾音
两种存储方式的取舍
把 JWT 放在 localStorage 实现简单,但任何 XSS 都能读走 token;放在 HttpOnly Cookie 中,JavaScript 无法读取,能显著降低 token 泄露风险,但需要处理 CSRF。
个人博客的后台只有一个管理员,攻击面较小,我选择了 HttpOnly Cookie + SameSite=Lax。
服务端实现
const token = jwt.sign({ sub: user.id, role: user.role }, config.jwtSecret, {
expiresIn: '7d',
});
res.cookie('blog_token', token, {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
maxAge: 7 * 24 * 3600 * 1000,
path: '/',
});
鉴权中间件从 Cookie 读取并校验:
export function requireAuth(req, res, next) {
const token = req.cookies[config.cookieName];
if (!token) return res.status(401).json({ message: '请先登录' });
try {
req.user = jwt.verify(token, config.jwtSecret);
next();
} catch {
res.status(401).json({ message: '登录已过期' });
}
}
CSRF 与暴力破解
SameSite=Lax 已经阻止了绝大多数跨站表单提交,但登录接口仍然需要限流:
- 同一 IP 15 分钟内最多 10 次登录尝试;
- 密码使用 bcrypt(cost=10)存储;
- 登录失败不区分"用户不存在"与"密码错误",避免枚举。
const hash = await bcrypt.hash(password, 10);
const ok = await bcrypt.compare(input, hash);
退出登录
退出时清除 Cookie 即可。JWT 本身无状态,无法"作废",因此过期时间不宜过长;如果需要强制下线,可以在用户表中记录 token_version,签发时写入并在校验时比对。
小结
没有绝对安全的方案,只有与威胁模型匹配的方案。对单人后台,HttpOnly Cookie + 限流 + bcrypt 已经提供了足够的保护,同时保持了实现上的简洁。