JWT认证时前端请求未携带req.cookies致401问题求助
问题背景
开发带有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),确认登录后
jwtCookie是否被正确存储,检查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

