# 用 Cookie+HttpOnly 实现双 Token 认证与滑动刷新

# 为什么需要双 Token 方案?

传统单 Token 方案面临两大痛点:

  1. 安全风险 - Token 泄露等于永久入侵
  2. 体验问题 - 频繁重新登录影响用户体验

双 Token 方案完美解决:

  • 🛡️ Access Token(短时效):15-30 分钟有效期,降低被盗风险
  • 🔄 Refresh Token(长时效):7-30 天有效期,静默刷新会话

# 技术选型

组件方案优势说明
Access TokenJWT + localStorage方便前端使用,自动过期
Refresh TokenHttpOnly Cookie防止 XSS 窃取,服务端可控
加密算法bcrypt

# 实现核心流程

🔑 认证流程全解析(→ 表示请求,← 表示响应)

1. 【登录阶段】
用户 → 前端:提交账号密码
前端 → 后端:POST /login
后端 ← 前端:
   │ HTTP Status 200
   ├─ Headers: Set-Cookie: refreshToken=xxx; HttpOnly
   └─ Body: { "accessToken": "jwt_token" }

2. 【正常请求】
用户 → 前端:发起数据请求
前端 → 后端:
   │ Headers: Authorization: Bearer accessToken
   ├─ 请求敏感数据
后端 ← 前端:
   │ HTTP Status 401(Token过期)
   └─ Body: { "error": "Token expired" }

3. 【自动刷新】
前端 → 后端:POST /refresh-token
   │ 自动携带 Cookie: refreshToken=xxx
后端 ← 前端:
   │ HTTP Status 200
   └─ Body: { "accessToken": "new_jwt_token" }

4. 【重试请求】
前端 → 后端:重新发送原始请求
   │ Headers: Authorization: Bearer new_jwt_token
后端 ← 前端:
   │ HTTP Status 200
   └─ Body: 请求数据

# 核心代码实现

# 后端关键代码

// 登录接口
router.post('/login', async (req, res) => {
  //... 验证用户名密码
  
  // 生成双 Token
  const accessToken = generateAccessToken(user);
  const refreshToken = generateRefreshToken(user);
  // 设置 HttpOnly Cookie(注意安全配置!)
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 天
  });
  res.json({ accessToken });
});
// 刷新接口
router.post('/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken; // 自动携带
  
  try {
    const decoded = verifyRefreshToken(refreshToken);
    const newAccessToken = generateAccessToken(decoded);
    res.json({ accessToken: newAccessToken });
  } catch (error) {
    res.clearCookie('refreshToken');
    res.status(401).end();
  }
});

# 前端拦截器

//axios 实例
const api = axios.create({
  baseURL: API_URL,
  withCredentials: true // 关键配置!
});
// 响应拦截器
api.interceptors.response.use(response => response, async error => {
  const originalRequest = error.config;
  
  if (error.response.status === 401 && !originalRequest._retry) {
    originalRequest._retry = true;
    
    try {
      const { data } = await api.post('/refresh');
      localStorage.setItem('accessToken', data.accessToken);
      return api(originalRequest);
    } catch (e) {
      logout();
    }
  }
  
  return Promise.reject(error);
});

# 避坑指南🚨

  1. CORS 配置:未启用 CORS 凭证
// 后端必须配置
app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true // 关键!
}));
  1. Cookie 参数:Cookie 参数配置错误
res.cookie('refreshToken', token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production', // 生产环境必须 true
  sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax'
});
  1. 缺少 cookie-parser 中间件
npm install cookie-parser
const cookieParser = require('cookie-parser');
app.use(cookieParser());
  1. 前端请求头:前端请求头配置错误
//axios 配置
withCredentials: true

# 滑动刷新 refreshToken 实现

什么是滑动刷新机制?
滑动刷新机制是指在用户执行某些关键操作(如进入房间、提交表单等)时,服务器会检查并更新 refreshToken 的有效期。这样可以确保用户在一段时间内保持活跃状态,而不会因为 refreshToken 过期而被迫重新登录。

  1. 从请求中获取 refreshToken
const refreshTokenFromCookie = req.cookies.refreshToken;
if (!refreshTokenFromCookie) {
    return res.status(400).json({ error: '刷新令牌缺失!' });
}
  1. 验证 refreshToken 是否有效
try {
    const decoded = verifyRefreshToken(refreshTokenFromCookie);
    if (!decoded) {
        return res.status(401).json({ error: '无效的刷新令牌!' });
    }
} catch (error) {
    console.error(error);
    return res.status(401).json({ error: '验证刷新令牌失败!' });
}
  1. 生成新的 refreshToken 并设置到 Cookie
    这里的 config.jwtRefreshTokenExpiry 是配置文件中的刷新令牌有效期,单位为秒。
const payload = { userId: decoded.userId, nickname: decoded.nickname };
const newRefreshToken = generateRefreshToken(payload);
const jwtExpiryInSeconds = parseInt(config.jwtRefreshTokenExpiry);
const cookieMaxAge = jwtExpiryInSeconds * 1000;
res.cookie('refreshToken', newRefreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
    domain: process.env.NODE_ENV === 'production' ? '.yourdomain.com' : 'localhost',
    path: '/',
    maxAge: cookieMaxAge,
});

# 总结

通过 HttpOnly Cookie + JWT 的双 Token 方案,我们实现了:

  • ✅ 更高的安全性(防 XSS 窃取)
  • ✅ 无缝的用户体验(静默刷新)
  • ✅ 灵活的控制能力(强制下线、设备管理)

完整代码 https://github.com/aynya/VoiceChat