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

JWT认证时前端请求未携带req.cookies致401问题求助

跨域环境下Refresh Token Cookie无法被后端读取的问题

问题背景

开发带有Refresh Token轮换机制的认证后端,在Insomnia中测试登录、注册、刷新Token流程完全正常,但浏览器调用/refresh接口时返回401 Unauthorized,后端打印req.cookies为空对象。开发环境下已尝试常见方案但无效,本人首次开发后端。

问题详情

  • 登录接口可正常返回JSON格式的accessToken,同时将refreshToken以HTTPOnly Cookie形式返回
  • 调用/refresh接口时,后端无法读取请求携带的Cookie,直接触发401错误
  • Insomnia测试时,MongoDB中的refreshToken能正常更新,/refresh接口可返回新的accessToken和refreshToken

已尝试的解决方案

  • Axios请求配置withCredentials: true
  • CORS配置添加credentials: true和origin: true
  • 将res.cookie()的secure参数设为false

相关代码片段

Server.js

require("dotenv").config({ path: "./.env" });
const express = require("express");
const app = express();
const connectDB = require("../server/configs/db");
const errorHandlerMiddleware = require("../server/middleware/error-handler");
const mongoose = require("mongoose");
const cors = require("cors");
const path = require("path");
const PORT = process.env.PORT || 5000;
const verifyJWT = require("./middleware/verifyJWT");
const cookieParser = require("cookie-parser");

//Connect Database
connectDB();
const db = mongoose.connection;

// Middleware
app.use(cors({ credentials: true, origin: true }));
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(errorHandlerMiddleware);

//Serve public folder
app.use("/", express.static(path.join(__dirname, "/public")));

//Routes
app.use("/api/users", require("./routes/authRoutes"));
app.use("/api/logout", require("./routes/logoutRoutes"));

//Protected routes
app.use(verifyJWT);
app.use("/api/habitcards", require("./routes/habitcardRoutes"));

db.once("open", () => {
  console.log("Connected to database");
  app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
  });
});

// Log errors on occurence

db.on("error", (err) => {
  console.log("Error connecting to database", err);
});

Refresh控制器

//Get the refresh token from the request
const cookies = req.cookies;
const refreshToken = cookies.jwt;

console.log(cookies);
console.log("step");

//Check if the refresh token is present
if (!refreshToken) {
  return res.status(401).json({ message: "Unauthorized" });
}
//Remove the old refresh token from the client
res.clearCookie("jwt", { httpOnly: true, secure: false, sameSite: "none" });

//Check if the user exists
const user = await User.findOne({ refreshToken }).select("-password").exec();

//If the user does not exist then the refreshToken does not exist anymore
// Check to which user the refreshToken (Refresh token container user ID) and delete all refresh tokens for that user
if (!user) {
  jwt.verify(
    refreshToken,
    process.env.REFRESH_TOKEN_SECRET,
    async (err, user) => {
      //Return Forbidden when token is invalid
      console.log("hoi");
      if (err) return res.status(403).json({ message: "Forbidden" });
      //Delete all refresh tokens for that user if token is valid (Maybe warn the user?)
      const hackedUser = await User.findById(decoded.userId);
      hackedUser.refreshToken = [];
      await hackedUser.save();
    }
  );
  return res.sendStatus(403);
}

//Make sure the new refreshtoken is not the same as the old one
const newRefreshTokenArray = user.refreshToken.filter(
  (rt) => rt !== refreshToken
);

jwt.verify(
  refreshToken,
  process.env.REFRESH_TOKEN_SECRET,
  asyncHandler(async (err, decoded) => {
    if (err) {
      user.refreshToken = [...newRefreshTokenArray];
      const result = await user.save();
    }

    if (err || user.email !== decoded.email)
      return res.status(403).json({ msg: "Forbidden" }); // Forbidden

    //Refresh token was still valid
    const roles = Object.values(user.roles);

    //Create a new accessToken and refreshToken and return to the user
    const accessToken = jwt.sign(
      { userInfo: { userId: user.id, roles: roles } },
      process.env.ACCESS_TOKEN_SECRET,
      {
        expiresIn: "30s",
      }
    );

    const newRefreshToken = jwt.sign(
      { userId: user.id, email: user.email },
      process.env.REFRESH_TOKEN_SECRET,
      {
        expiresIn: "1d",
      }
    );

    //Save the new refresh token to the user
    user.refreshToken = [...newRefreshTokenArray, newRefreshToken];
    const result = await user.save();

    res.cookie("jwt", newRefreshToken, {
      httpOnly: true,
      secure: false,
      sameSite: "none",
      maxAge: 24 * 60 * 60 * 1000,
    });

    res.json({ accessToken });
  })
);
};

前端Refresh请求代码(调试简化版)

const useAxios = axios.create({
  baseURL: "http://localhost:5000/api/users",
  withCredentials: true,
  crossDomain: true,
});

const refresh = async () => {
  try {
    const { data } = await useAxios.get("/refresh");
    console.log(data);
  } catch (error) {
    console.log(error);
  }
};

可尝试的解决办法

  • 明确指定CORS origin:将CORS配置中的origin: true替换为前端实际地址(如"http://localhost:3000"),浏览器对动态origin的Cookie传递限制更严格,明确指定可减少跨域校验问题。修改后代码:

    app.use(cors({ credentials: true, origin: "http://localhost:3000" }));
    
  • 调整Cookie的sameSite参数:开发环境下若前后端均为localhost端口族,可将sameSite从"none"改为"lax"或"strict"。none通常配合secure: true使用,即使设置了secure: false,部分浏览器仍会存在兼容性问题。

  • 检查浏览器Cookie存储状态:打开开发者工具(Application -> Cookies),确认登录后jwt Cookie是否被正确存储,检查Domain、Path、HttpOnly等属性是否符合预期。若Cookie未存储,说明登录接口的Cookie设置存在问题。

  • 统一res.clearCookie的参数:调用res.clearCookie时,需携带与设置Cookie时完全一致的参数(除maxAge),建议添加path: "/"确保Cookie在整个站点生效:

    res.clearCookie("jwt", { httpOnly: true, secure: false, sameSite: "none", path: "/" });
    
  • 确认路由未被错误拦截:检查/api/users/refresh是否被verifyJWT中间件意外拦截,确保该接口无需前置认证即可访问。

  • 显式设置单个请求的withCredentials:在refresh接口请求中再次明确添加withCredentials: true:

    const { data } = await useAxios.get("/refresh", { withCredentials: true });
    

内容的提问来源于stack exchange,提问作者tomtom500

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 20:15:15