如何在用户活跃时无感知后台刷新JWT Access Token?
问题描述
我正在为应用实现用户认证功能,Access Token有效期30分钟,Refresh Token有效期7天。需求如下:
- 用户在网站活跃时,Access Token过期后需后台自动续期,不打扰用户,保证操作无缝
- 用户关闭页面后返回,进入时自动刷新Token
- 技术栈:Node.js后端 + React.js前端
- 要求:后台更新Token,不通知用户或跳转页面
现有后端代码
userAuth.js
const User = require("../../models/User"); const OTP = require("../../models/otp"); const bcrypt = require("bcryptjs"); const crypto = require("crypto"); const { generateAccessToken, generateRefreshToken, verifyRefreshToken, } = require("../../util/jwtUtile"); require("dotenv").config(); const nodemailer = require("nodemailer"); // 配置nodemailer const transporter = nodemailer.createTransport({ service: "Gmail", auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS, }, }); // 生成数字OTP const generateOTP = () => { return Math.floor(100000 + Math.random() * 900000).toString(); // 生成6位数字OTP }; // 注册 exports.registerUser = async (req, res) => { const { username, email, password } = req.body; try { const hashedPassword = await bcrypt.hash(password, 10); const otp = generateOTP(); // 将OTP存储到数据库 const otpEntry = new OTP({ email, otp }); await otpEntry.save(); // 发送OTP到用户邮箱 const mailOptions = { from: process.env.EMAIL_USER, to: email, subject: "注册验证码", text: `你的验证码是 ${otp}`, }; await transporter.sendMail(mailOptions); res.status(201).json({ msg: "验证码已发送至你的邮箱" }); } catch (error) { console.error("错误:", error.message); res.status(400).json({ error: error.message }); } }; // 验证OTP exports.verifyOTP = async (req, res) => { const { email, otp } = req.body; try { const otpEntry = await OTP.findOne({ email, otp }); if (!otpEntry) { return res.status(400).json({ msg: "验证码或邮箱无效" }); } // OTP验证通过,完成注册 const storedData = otpEntry; const newUser = new User({ username: storedData.username, email: storedData.email, password: storedData.password, }); const user = await newUser.save(); await OTP.deleteOne({ email, otp }); // 删除OTP记录 console.log("用户注册成功"); res.status(201).json({ msg: "用户注册成功", user }); } catch (error) { console.error("错误:", error.message); res.status(400).json({ error: error.message }); } }; // 登录 exports.loginUser = async (req, res) => { const { email, password } = req.body; try { const user = await User.findOne({ email }); if (!user) { return res.status(400).json({ msg: "用户名或密码无效" }); } const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { return res.status(400).json({ msg: "用户名或密码无效" }); } const accessToken = generateAccessToken({ id: user._id, isAdmin: user.isAdmin, }); const refreshToken = generateRefreshToken({ id: user._id, isAdmin: user.isAdmin, }); res.cookie("accessToken", accessToken, { httpOnly: true, secure: true }); res.cookie("refreshToken", refreshToken, { httpOnly: true }); res.status(200).json({ msg: "登录成功" }); } catch (error) { console.error("错误:", error.message); res.status(500).json({ error: error.message }); } };
refreshToken.js
const { generateAccessToken, verifyRefreshToken } = require("./jwtUtile"); // 刷新令牌 exports.refreshToken = (req, res) => { const refreshToken = req.cookies.refreshToken; if (!refreshToken) return res.status(403).json({ error: "未提供刷新令牌" }); try { const user = verifyRefreshToken(refreshToken); const newAccessToken = generateAccessToken({ id: user.id, isAdmin: user.isAdmin, }); res.cookie("accessToken", newAccessToken, { httpOnly: true, secure: true }); } catch (err) { return res .status(401) .json({ error: "刷新令牌已过期,请重新登录" }); } };
解决方案
后端优化
1. 完善刷新令牌接口
为refreshToken.js添加Cookie有效期配置,并在刷新失败时清除无效的Refresh Token:
const { generateAccessToken, verifyRefreshToken } = require("./jwtUtile"); exports.refreshToken = (req, res) => { const refreshToken = req.cookies.refreshToken; if (!refreshToken) { return res.status(403).json({ error: "未提供刷新令牌" }); } try { const user = verifyRefreshToken(refreshToken); const newAccessToken = generateAccessToken({ id: user.id, isAdmin: user.isAdmin, }); res.cookie("accessToken", newAccessToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", // 仅生产环境开启HTTPS安全Cookie maxAge: 30 * 60 * 1000 // 与AccessToken有效期一致 }); res.status(200).json({ msg: "令牌刷新成功" }); } catch (err) { // 清除无效的Refresh Token res.clearCookie("refreshToken", { httpOnly: true }); return res.status(401).json({ error: "刷新令牌已过期,请重新登录" }); } };
2. 全局认证中间件
创建自动处理Token过期与刷新的中间件,所有需要认证的接口挂载此中间件即可:
// middleware/authMiddleware.js const { verifyAccessToken } = require("../util/jwtUtile"); const { refreshToken } = require("../controllers/refreshToken"); exports.authenticate = async (req, res, next) => { const accessToken = req.cookies.accessToken; if (!accessToken) { // 无AccessToken,尝试直接刷新 try { await refreshToken(req, res, () => {}); const newAccessToken = req.cookies.accessToken; const user = verifyAccessToken(newAccessToken); req.user = user; next(); } catch (err) { return res.status(401).json({ error: "请先登录" }); } } else { try { // 验证AccessToken有效性 const user = verifyAccessToken(accessToken); req.user = user; next(); } catch (err) { // AccessToken过期,尝试用Refresh Token刷新 try { await refreshToken(req, res, () => {}); const newAccessToken = req.cookies.accessToken; const user = verifyAccessToken(newAccessToken); req.user = user; next(); } catch (refreshErr) { return res.status(401).json({ error: "登录已过期,请重新登录" }); } } } };
前端实现
1. 页面加载时自动刷新Token
在App根组件中添加页面加载时的Token刷新逻辑:
// src/App.js import { useEffect } from 'react'; import axios from 'axios'; function App() { useEffect(() => { const refreshOnLoad = async () => { try { // 调用刷新接口,自动更新AccessToken await axios.get('/api/refresh-token', { withCredentials: true }); } catch (err) { // 刷新失败则跳转登录页 if (err.response?.status === 401) { window.location.href = '/login'; } } }; refreshOnLoad(); }, []); return ( // 你的应用内容 ); } export default App;
2. 用户活跃时自动续期
创建自定义Hook监听用户交互,提前刷新即将过期的Token:
// src/hooks/useTokenRefresh.js import { useEffect } from 'react'; import axios from 'axios'; // 解析AccessToken获取过期时间(毫秒) const getTokenExpiry = () => { const accessToken = document.cookie.split('; ').find(row => row.startsWith('accessToken='))?.split('=')[1]; if (!accessToken) return null; const payload = JSON.parse(atob(accessToken.split('.')[1])); return payload.exp * 1000; }; // 刷新Token const refreshToken = async () => { try { await axios.get('/api/refresh-token', { withCredentials: true }); } catch (err) { if (err.response?.status === 401) { window.location.href = '/login'; } } }; export const useTokenRefresh = () => { useEffect(() => { // 设置刷新计时器(提前5分钟刷新) const setupTimer = () => { const expiry = getTokenExpiry(); if (!expiry) return; const timeUntilRefresh = expiry - Date.now() - 5 * 60 * 1000; if (timeUntilRefresh > 0) { setTimeout(refreshToken, timeUntilRefresh); } else { refreshToken(); } }; setupTimer(); // 监听用户交互,重置刷新计时器 const handleActivity = () => setupTimer(); window.addEventListener('click', handleActivity); window.addEventListener('scroll', handleActivity); window.addEventListener('keydown', handleActivity); return () => { window.removeEventListener('click', handleActivity); window.removeEventListener('scroll', handleActivity); window.removeEventListener('keydown', handleActivity); }; }, []); };
在App组件中使用该Hook:
// src/App.js import { useEffect } from 'react'; import axios from 'axios'; import { useTokenRefresh } from './hooks/useTokenRefresh'; function App() { useTokenRefresh(); useEffect(() => { const refreshOnLoad = async () => { try { await axios.get('/api/refresh-token', { withCredentials: true }); } catch (err) { if (err.response?.status === 401) { window.location.href = '/login'; } } }; refreshOnLoad(); }, []); return ( // 你的应用内容 ); } export default App;
内容的提问来源于stack exchange,提问作者Kuldeep Sonara
相关产品推荐
相关产品推荐

