Stack Horizon

JWT 究竟是什么

JWT(JSON Web Token)是一种紧凑且 URL 安全的声明传输方式,用于在两方之间传递声明。它不是魔法般的安全解决方案,而是一种令牌格式:由三个 base64url 编码的段组成,以点号分隔(header.payload.signature)。

header: {"alg":"HS256","typ":"JWT"}
payload: {"sub":"user123","iat":1700000000}
signature: HMACSHA256(base64url(header)+"."+base64url(payload), secret)

Enter fullscreen mode Exit fullscreen mode

常见错误:将 JWT 存储在 localStorage 中

不要这样做。localStorage 可通过 JavaScript 访问(XSS)。如果攻击者注入脚本,就会窃取你的令牌。改用 httpOnly cookie。cookie 会随请求自动发送,且无法被 JS 读取。

// Bad: vulnerable to XSS
localStorage.setItem('token', jwt);

// Better: httpOnly cookie (set by server)
Set-Cookie: token=<jwt>; HttpOnly; Secure; SameSite=Strict

Enter fullscreen mode Exit fullscreen mode

如何实现 JWT 认证(实用方法)

1. 登录端点在 cookie 中签发 JWT

app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = authenticate(username, password);
  if (!user) return res.status(401).send('Invalid credentials');

  const token = jwt.sign(
    { sub: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );

  res.cookie('token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 15 * 60 * 1000
  });
  res.json({ message: 'Logged in' });
});

Enter fullscreen mode Exit fullscreen mode

2. 中间件在每次请求时验证令牌

function authMiddleware(req, res, next) {
  const token = req.cookies.token;
  if (!token) return res.status(401).send('No token');

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).send('Invalid token');
  }
}

app.get('/protected', authMiddleware, (req, res) => {
  res.json({ user: req.user });
});

Enter fullscreen mode Exit fullscreen mode

3. 使用刷新令牌实现更长会话

访问令牌有效期较短(15 分钟)。使用刷新令牌(长期有效,存储在单独的 httpOnly cookie 中)来获取新的访问令牌,而无需再次输入凭据。

app.post('/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) return res.status(401).send('No refresh token');

  try {
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
    const newAccessToken = jwt.sign(
      { sub: decoded.sub },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );
    res.cookie('token', newAccessToken, { httpOnly: true, secure: true, sameSite: 'strict' });
    res.json({ message: 'Token refreshed' });
  } catch (err) {
    return res.status(403).send('Invalid refresh token');
  }
});

Enter fullscreen mode Exit fullscreen mode

JWT 无法解决的问题

  • CSRF:如果使用 cookie,需要 CSRF 防护。可使用 SameSite=Strict 或 CSRF 令牌。
  • 令牌吊销:JWT 是无状态的。除非维护黑名单(这会破坏无状态性),否则无法吊销单个令牌。登出时,可在服务端清除 cookie 或使用较短的过期时间。
  • 载荷篡改:签名可确保完整性。未验证签名前,切勿信任载荷。

常见陷阱

  • 在代码中存储密钥:使用环境变量。
  • 使用弱密钥:使用长且随机的字符串(例如 openssl rand -hex 64)。
  • 未验证算法:如果服务器接受 'none' 算法,攻击者即可伪造令牌。请始终指定允许的算法。
// Explicitly set algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] });

Enter fullscreen mode Exit fullscreen mode

总结

JWT 是一种工具,而非万能解决方案。请使用 httpOnly cookie、短生命周期令牌、刷新令牌以及适当的 CSRF 防护。理解权衡:无状态性与吊销能力。保持简单。