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

Node Express+React的Http Only Cookie+JWT认证遇SameSite及跨域问题

跨域Http Only Cookie身份验证问题解决

问题背景

我有一个基于Node Express的JWT身份验证服务器,原本前端把token存在localStorage,现在要切换到Http Only Cookie方案。服务器部署在Heroku,React前端在Netlify。已经配置好CORS,认证路由里用res.cookie设置了httpOnly: true、secure: true、sameSite: None,前端fetch请求也加了credentials: 'include',但遇到两个问题:

  1. 设置sameSite: None时,Heroku日志报错:Error creating JWT token: TypeError: option sameSite is invalid,JWT无法创建
  2. 把sameSite设为lax或strict时,JWT能发送到浏览器,但请求/users等接口时Cookie不会回传给服务器

相关代码

App.js

app.set("trust proxy", 1); // trust first proxy

const corsOptions = {
  origin: [
    "http://localhost:3000",
    "http://localhost:3001",
    "http://localhost:3002",
    "https://techhype.netlify.app",
    "https://techhype.no",
    "https://techhype-server-06db1c82ee3e.herokuapp.com",
  ],
  preflightContinue: false,
  optionsSuccessStatus: 204,
  allowedHeaders: ["Content-Type", "Authorization"],
  methods: ["GET", "POST", "PUT", "DELETE"],
  credentials: true,
};

app.use(cors(corsOptions));

auth.js路由

router.post("/login", authLimiter, jsonParser, async (req, res, next) => {
  const { email, password } = req.body;
  if (email == null) {
    return res.jsend.fail({ statusCode: 400, email: "Email is required." });
  }
  if (password == null) {
    return res.jsend.fail({
      statusCode: 400,
      password: "Password is required.",
    });
  }

  // Retrieve the user by email
  const user = await userService.getOneByEmail(email);
  if (!user) {
    return res.jsend.fail({
      statusCode: 400,
      result: "Incorrect email or password",
    });
  }

  // Check if the user is verified
  if (!user.Verified) {
    return res.jsend.fail({
      statusCode: 400,
      result: "User is not verified. Please check your email for the verification link.",
    });
  }

  // Verify the password
  crypto.pbkdf2(password, user.Salt, 310000, 32, "sha256", function (err, hashedPassword) {
    if (err) {
      return res.jsend.error("Error in password encryption");
    }
    if (!crypto.timingSafeEqual(user.EncryptedPassword, hashedPassword)) {
      return res.jsend.fail({ result: "Incorrect password" });
    }

    try {
      const token = jwt.sign(
        {
          id: user.id,
          email: user.Email,
          role: user.Role,
          Verified: user.Verified,
          name: user.FirstName,
        },
        process.env.TOKEN_SECRET,
        { expiresIn: process.env.JWT_EXPIRATION }  // or "3600" (expressed in seconds)
      );

      const refreshToken = jwt.sign(
        { id: user.id },
        process.env.REFRESH_TOKEN_SECRET,
        { expiresIn: process.env.JWT_EXPIRATION_LONG }  // or "604800" (expressed in seconds)
      );

      console.log("Token creation: ", token);

      // Set the JWT and refresh token in HTTP-only cookies
      res.cookie('token', token, {
        httpOnly: true,
        secure: true, //true in production only
        sameSite: 'None', // can be 'strict' or 'lax or None',
      });
      res.cookie('refreshToken', refreshToken, {
        httpOnly: true,
        secure: true,
        sameSite: 'None',
      });

      return res.jsend.success({
        result: "You are logged in",
        id: user.id,
        email: user.Email,
        name: user.FirstName,
        role: user.Role,
        verified: user.Verified
      });
    } catch (err) {
      console.error("Error creating JWT token:", err);
      return res.jsend.error("Something went wrong with creating the JWT token");
    }
  });
});

isAuth中间件

const jwt = require("jsonwebtoken");

// Middleware function to determine if the API endpoint request is from an authenticated user
function isAuth(req, res, next) {
  const token = req.cookies.token;
  console.log("Received Token:", token); // Log the received token
  if (token) {
    try {
      const decodedToken = jwt.verify(token, process.env.TOKEN_SECRET);
      req.user = decodedToken; // Attach user data to the request object
      next();
    } catch (error) {
      res.status(401).json({ status: "fail", statusCode: 401, error: "Authentication failed" });
    }
  } else {
    res.status(401).json({ status: "fail", statusCode: 401, error: "No token provided" });
  }
}

module.exports = isAuth;

React前端获取用户数据代码

const fetchUserData = async () => {
  try {
      const response = await fetch(`${process.env.REACT_APP_API_URL}/users`, {
          method: 'GET',
          credentials: 'include', // Ensure cookies are included
      });
      const data = await response.json(); // Always parse JSON to check for error details
      console.log("user data response:", data);
      
      if (response.ok) {
          updateUser(data.data.result.user);
      } else {
          console.error('Failed to fetch user data', data);
          throw new Error(data.message || 'Failed to fetch user data');
      }
  } catch (error) {
      console.error('Fetch user data error:', error);
  }
};

解决方案

1. 修复sameSite: None无效的问题

报错原因是你的Express版本低于4.16.0,该版本才正式支持sameSite: 'None'参数。两种解决方式:

  • 升级Express:执行命令npm install express@latest,确保版本≥4.16.0
  • 兼容旧版本写法:手动构造Set-Cookie响应头,替代res.cookie:
// 替换原来的res.cookie代码块
const tokenCookie = `token=${token}; HttpOnly; Secure; SameSite=None; Path=/; Max-Age=${parseInt(process.env.JWT_EXPIRATION)}`;
const refreshTokenCookie = `refreshToken=${refreshToken}; HttpOnly; Secure; SameSite=None; Path=/; Max-Age=${parseInt(process.env.JWT_EXPIRATION_LONG)}`;
res.setHeader('Set-Cookie', [tokenCookie, refreshTokenCookie]);

2. 确保跨域Cookie回传的完整配置

  • 服务器端:
    • 确认app.set("trust proxy", 1);已开启,Heroku作为反向代理,开启后Express才能识别真实请求协议为HTTPS,保证Secure属性生效(浏览器要求SameSite=None必须搭配Secure)
    • CORS的origin必须精确匹配前端域名,不能用通配符,credentials: true必须保持开启
  • 前端:
    • 确认fetch的credentials: 'include'正确设置,你的代码已符合要求
  • 环境变量:在Heroku设置NODE_ENV=production,确保secure: true只在生产环境生效(本地开发可设为false)

3. 修正后的res.cookie写法(适用于新版Express)

res.cookie('token', token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'None',
  path: '/', // 确保Cookie在所有接口路径下可用
  maxAge: parseInt(process.env.JWT_EXPIRATION) * 1000 // 和JWT过期时间保持一致
});
res.cookie('refreshToken', refreshToken, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'None',
  path: '/',
  maxAge: parseInt(process.env.JWT_EXPIRATION_LONG) * 1000
});

排查要点

  1. 查看浏览器开发者工具→Application→Cookies,确认Cookie的SameSite为None、Secure已勾选、Domain正确指向Heroku服务器域名
  2. 检查Network标签:登录请求的Response Headers是否包含正确的Set-Cookie,/users请求的Request Headers是否携带Cookie字段
  3. 关闭广告拦截类浏览器插件,部分插件会阻止跨域Cookie发送

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 03:42:03