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

生产环境下React调用Express API时Token/Cookie未定义问题

React + Express 跨域认证生产环境异常问题

问题现象

  • 后端部署在Heroku平台,前端部署在Netlify平台,本地开发阶段功能完全正常,生产环境出现认证异常
  • 用户可正常完成登录操作,但无法访问个人资料页面,接口返回提示token/cookie未定义
  • 登录成功时Cookie中存在token,但点击页面链接或刷新页面后token消失,导致所有需要用户认证的功能无法使用

相关代码片段

后端登录接口实现

const loginUser = async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }

  const { email, password } = req.body;

  try {
    // 检查用户是否存在
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(400).json({ msg: "Invalid credentials" });
    }

    // 验证密码是否匹配
    const isMatch = await bcrypt.compare(password, user.password);
    if (!isMatch) {
      return res.status(400).json({ msg: "Invalid credentials" });
    }

    // 创建JWT payload
    const payload = {
      user: {
        id: user.id,
        role: user.role,
      },
    };

    // 签发JWT
    jwt.sign(
      payload,
      process.env.JWT_SECRET,
      { expiresIn: "2h" }, // 有效期2小时
      (err, token) => {
        if (err) throw err;

        // 设置HTTP-only Cookie
        res.cookie("token", token, {
          httpOnly: true,
          secure: process.env.NODE_ENV === "production", // 生产环境启用secure
          sameSite: "strict", // 防止CSRF
          maxAge: 2 * 60 * 60 * 1000, // 2小时有效期
        });

        // 返回用户数据和token
        res.status(200).json({
          user: {
            id: user.id,
            email: user.email,
            first_name: user.first_name,
            last_name: user.last_name,
            role: user.role,
          },
          token: token,
        });
      }
    );
  } catch (err) {
    console.error(err.message);
    res.status(500).json({ msg: "Server error" });
  }
};

后端路由配置

router.post(
  "/api/login",
  [
    check("email", "Please include a valid email").isEmail(),
    check("password", "Password is required").exists(),
  ],
  userController.loginUser
);

前端登录组件

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";

const UserLoginForm = ({ setUser }) => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const navigate = useNavigate();

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await axios.post(
        "https://app.herokuapp.com/users/api/login",
        { email, password },
        { withCredentials: true }
      );
      setUser(response.data.user);
      console.log(response.data);
      localStorage.setItem("user", JSON.stringify(response.data.user));
      navigate("/");
    } catch (err) {
      setError(err.response?.data?.message || "An error occurred during login");
    }
  };

  return (
    <div className="min-h-screen bg-gray-100 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
      <div className="sm:mx-auto sm:w-full sm:max-w-md">
        <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
          Sign in to your account
        </h2>
      </div>

      <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
        <div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
          <form className="space-y-6" onSubmit={handleSubmit}>
            <div>
              <label
                htmlFor="email"
                className="block text-sm font-medium text-gray-700"
              >
                Email address
              </label>
              <div className="mt-1">
                <input
                  id="email"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                />
              </div>
            </div>

            <div>
              <label
                htmlFor="password"
                className="block text-sm font-medium text-gray-700"
              >
                Password
              </label>
              <div className="mt-1">
                <input
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="current-password"
                  required
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                />
              </div>
            </div>

            <div>
              <button
                type="submit"
                className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
              >
                Sign in
              </button>
            </div>
          </form>

          {error && (
            <div className="mt-4 text-center text-red-600">{error}</div>
          )}
        </div>
      </div>
    </div>
  );
};

export default UserLoginForm;

认证中间件

const jwt = require("jsonwebtoken");
const User = require("../models/User");
const Salon = require("../models/Salon");

const authMiddleware = async (req, res, next) => {
  console.log(req.cookies, "this is cookies");
  try {
    // 从Cookie或请求头获取token
    const token = req.cookies.token || req.header("x-auth-token");

    // 无token时设置未认证状态
    if (!token) {
      req.user = null;
      req.salon = null;
      res.locals.authenticatedEntityId = null;
      return next();
    }

    // 验证token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    if (decoded.user) {
      // 处理用户认证
      const user = await User.findById(decoded.user.id).select("-password");
      if (!user) throw new Error("User not found");
      
      req.user = user;
      req.salon = null;
      res.locals.authenticatedEntityId = user._id;
    } else if (decoded.salon) {
      // 处理商家认证
      const salon = await Salon.findById(decoded.salon.id).select("-password");
      if (!salon) throw new Error("Salon not found");
      
      req.salon = salon;
      req.user = null;
      res.locals.authenticatedEntityId = salon._id;
    } else {
      // 无效token
      req.user = null;
      req.salon = null;
      res.locals.authenticatedEntityId = null;
    }

    next();
  } catch (error) {
    console.error("Auth Middleware Error:", error);
    req.user = null;
    req.salon = null;
    res.locals.authenticatedEntityId = null;
    next();
  }
};

module.exports = authMiddleware;

后端app.js配置

app.use(
  cors({
    origin: "https://client.netlify.app", // 允许Netlify前端请求
    credentials: true, // 允许携带凭证
    methods: ["GET", "POST", "PUT", "DELETE"], // 允许的请求方法
  })
);

// 创建HTTP服务器并配置Socket.IO
const server = http.createServer(app);
const io = socketIo(server, {
  cors: {
    origin: "https://client.netlify.app",
    methods: ["GET", "POST", "PUT", "DELETE"],
    credentials: true,
  },
});

app.set("io", io);

// Socket.IO事件监听
io.on("connection", (socket) => {
  console.log("New client connected");

  socket.on("newComment", (data) => {
    io.emit("updateComments", data);
  });

  socket.on("newReply", (data) => {
    io.emit("updateReplies", data);
  });

  socket.on("disconnect", () => {
    console.log("Client disconnected");
  });
});

// MongoDB连接
mongoose
  .connect(process.env.MONGO_URI, {})
  .then(() => console.log("MongoDB connected successfully"))
  .catch((err) => console.error("MongoDB connection error:", err));

// 视图引擎配置
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");

app.use("/webhook", bodyParser.raw({ type: "application/json" }));
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));

// Session配置
app.use(
  session({
    secret: process.env.JWT_SECRET,
    resave: false,
    saveUninitialized: false,
    store: MongoStore.create({ mongoUrl: process.env.MONGO_URI }),
    cookie: {
      maxAge: 1000 * 60 * 60 * 24, // 1天有效期
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "strict",
    },
  })
);

问题原因及修复方案

核心问题

  1. SameSite属性不兼容跨域场景:生产环境下前后端分属不同域名,sameSite: "strict"会阻止浏览器在跨域请求中携带Cookie,导致跳转/刷新后后端无法获取token
  2. Cookie未配置Domain属性:跨域场景下未指定Domain,浏览器仅在当前域名下携带Cookie,后续请求不会传递到后端
  3. 前端axios未全局配置凭证携带:仅登录请求设置了withCredentials: true,其他API请求未配置,导致后续请求不携带Cookie
  4. Session与JWT Cookie冲突:同时启用Session和JWT Cookie认证,可能导致Cookie存储或传递异常

修复步骤

1. 修改后端Cookie配置

调整登录接口中res.cookie的参数,适配跨域场景:

res.cookie("token", token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === "production",
  sameSite: process.env.NODE_ENV === "production" ? "none" : "strict", // 生产环境用none兼容跨域
  domain: process.env.NODE_ENV === "production" ? ".herokuapp.com" : undefined, // 配置Heroku根域名
  maxAge: 2 * 60 * 60 * 1000,
});

注意:sameSite: "none"必须配合secure: true使用,仅HTTPS环境生效

2. 全局配置axios携带凭证

在前端项目中创建axios全局实例,统一配置withCredentials:

// src/utils/axios.js
import axios from 'axios';

const api = axios.create({
  baseURL: "https://app.herokuapp.com",
  withCredentials: true, // 全局开启凭证携带
});

export default api;

后续所有API请求均使用该实例,替代直接调用axios

3. 移除Session中间件(可选)

当前使用JWT Cookie认证,Session配置无必要,可注释或移除:

// 注释以下代码
// app.use(
//  session({
//    secret: process.env.JWT_SECRET,
//    resave: false,
//    saveUninitialized: false,
//    store: MongoStore.create({ mongoUrl: process.env.MONGO_URI }),
//    cookie: {
//      maxAge: 1000 * 60 * 60 * 24,
//      httpOnly: true,
//      secure: process.env.NODE_ENV === "production",
//      sameSite: "strict",
//    },
//  })
// );

4. 添加页面刷新时的认证校验

在App组件中添加刷新时的用户状态校验,避免localStorage数据与后端状态不一致:

// App.js
import { useEffect } from 'react';
import api from './utils/axios';

function App({ setUser }) {
  useEffect(() => {
    const checkAuthStatus = async () => {
      const savedUser = localStorage.getItem("user");
      if (savedUser) {
        try {
          const res = await api.get("/users/api/me"); // 后端需实现获取当前用户接口
          setUser(res.data.user);
        } catch (err) {
          localStorage.removeItem("user");
          setUser(null);
        }
      }
    };
    checkAuthStatus();
  }, [setUser]);

  // ... 其他组件逻辑
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 22:20:53