You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在用户活跃时无感知后台刷新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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 05:45:55