不再用localStorage:2025年用Cookie管理JWT令牌的最佳方法

摘要:很多开发者习惯把JWT令牌存在localStorage里,但这样做其实有安全风险。如果网站存在XSS漏洞,攻击者可以轻易偷走令牌。换个方法,把令牌放在HttpOnly Cookie里,能解决这个问题。

很多开发者习惯把JWT令牌存在localStorage里,但这样做其实有安全风险。如果网站存在XSS漏洞,攻击者可以轻易偷走令牌。换个方法,把令牌放在HttpOnly Cookie里,能解决这个问题。

HttpOnly Cookie有三个好处:

  1. JavaScript无法读取,XSS攻击偷不走令牌

  2. 浏览器自动在每次请求中携带,不需要手动设置请求头

  3. 可以使用安全标志,降低CSRF和窃听风险

只需要一些配置,就能大幅提升安全性。


服务器端配置:Node.js中签发和刷新令牌

先搭建一个简单的Express服务器:

// server/index.mjs
import express from 'express';
import cookieParser from 'cookie-parser';
import csurf from 'csurf';

const app = express();
app.use(express.json());
app.use(cookieParser());

// 设置CSRF保护
app.use(csurf({ cookie: true }));

// 获取CSRF令牌的接口
app.get('/api/csrf-token', (req, res) => {
  res.setHeader('x-csrf-token', req.csrfToken());
  res.sendStatus(204);
});

这段代码设置了JSON解析、Cookie解析和CSRF保护。之后所有POST、PUT、DELETE请求都需要提供CSRF令牌。


登录接口:生成访问令牌和刷新令牌

import jwt from 'jsonwebtoken';

const isProd = process.env.NODE_ENV === 'production';

app.post('/api/token', (req, res) => {
  const { username, password } = req.body;

  // 这里应该验证用户名和密码
  if (!isValidUser(username, password)) {
    return res.status(401).json({ error: '用户名或密码错误' });
  }

  const accessToken = jwt.sign({ sub: username }, process.env.ACCESS_SECRET, { 
    expiresIn: '15m' 
  });
  
  const refreshToken = jwt.sign({ sub: username }, process.env.REFRESH_SECRET, { 
    expiresIn: '7d' 
  });

  res
    .cookie('access_token', accessToken, {
      httpOnly: true,
      sameSite: 'strict',
      secure: isProd,
      maxAge: 15 * 60 * 1000
    })
    .cookie('refresh_token', refreshToken, {
      httpOnly: true,
      sameSite: 'strict',
      secure: isProd,
      maxAge: 7 * 24 * 60 * 60 * 1000
    })
    .json({ user: username, status: 'active' });
});

验证通过后生成两个令牌,都设置为HttpOnly Cookie,JavaScript无法读取,但浏览器会自动发送。


刷新令牌接口

app.post('/api/token/refresh', (req, res) => {
  const refreshToken = req.cookies.refresh_token;
  if (!refreshToken) return res.sendStatus(401);

  jwt.verify(refreshToken, process.env.REFRESH_SECRET, (err, decoded) => {
    if (err) return res.sendStatus(403);

    const newAccess = jwt.sign({ sub: decoded.sub }, process.env.ACCESS_SECRET, { 
      expiresIn: '15m' 
    });
    
    const newRefresh = jwt.sign({ sub: decoded.sub }, process.env.REFRESH_SECRET, { 
      expiresIn: '7d' 
    });

    res
      .cookie('access_token', newAccess, {
        httpOnly: true,
        sameSite: 'strict',
        secure: isProd,
        maxAge: 15 * 60 * 1000
      })
      .cookie('refresh_token', newRefresh, {
        httpOnly: true,
        sameSite: 'strict',
        secure: isProd,
        maxAge: 7 * 24 * 60 * 60 * 1000
      })
      .json({ ok: true });
  });
});

这个接口用刷新令牌来获取新的访问令牌,保持用户登录状态。


前端React应用的使用方法

获取CSRF令牌:

async function getCSRF() {
  const res = await fetch('/api/csrf-token', { 
    credentials: 'include' 
  });
  return res.headers.get('x-csrf-token');
}

登录函数:

const loginUser = async (formData) => {
  const csrf = await getCSRF();

  const res = await fetch('/api/token', {
    method: 'POST',
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrf
    },
    body: JSON.stringify(formData)
  });

  if (res.ok) {
    const data = await res.json();
    // 只存储非敏感信息
    localStorage.setItem('username', data.user);
  } else {
    throw new Error('登录失败');
  }
};

自动刷新令牌:

useEffect(() => {
  const refreshInterval = setInterval(async () => {
    try {
      const csrf = await getCSRF();
      await fetch('/api/token/refresh', {
        method: 'POST',
        credentials: 'include',
        headers: { 
          'Content-Type': 'application/json', 
          'X-CSRF-Token': csrf 
        }
      });
    } catch (error) {
      console.log('令牌刷新失败');
    }
  }, 14 * 60 * 1000); // 每14分钟刷新一次

  return () => clearInterval(refreshInterval);
}, []);


实际应用场景

  1. 文件上传

const uploadFile = async (file) => {
  const formData = new FormData();
  formData.append('file', file);

  await fetch('/api/upload', {
    method: 'POST',
    body: formData,
    credentials: 'include'
  });
};

  1. Next.js服务端渲染

export async function getServerSideProps({ req }) {
  const token = req.cookies.access_token;
  // 用令牌获取用户数据
  return { props: { userData } };
}

  1. WebSocket连接

const socket = new WebSocket(
  `wss://example.com/chat?token=${getTokenFromCookie()}`
);

  1. 移动端React Native

// 使用Keychain或SecureStore存储令牌
import Keychain from 'react-native-keychain';

const storeToken = async (token) => {
  await Keychain.setGenericPassword('token', token);
};


安全建议

  1. 设置合适的令牌过期时间

  2. 生产环境一定要启用Secure标志

  3. 使用SameSite防止CSRF攻击

  4. 定期轮换密钥

  5. 监控异常登录行为


总结

从localStorage切换到HttpOnly Cookie管理JWT令牌,虽然需要一些额外配置,但能显著提高应用安全性。这种方法适合大多数Web应用,特别是需要较高安全要求的项目。

现代浏览器都支持这些特性,现在就开始升级你的认证系统吧。记住,安全是一个持续的过程,需要定期审查和更新安全措施。


本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://shenqiku.cn/article/FLY_12907