有Set-Cookie返回,但就是无法成功设置(最终解决方法)

为了解决(搞明白)这个问题,实在花了我超级长的时间,由于我是熬夜写代码,一写就是很长一段时间,甚至一两天。以至于我一直盯着我的前后端代码,怎么都看不出来个所以然。

Cover

即便查阅了大量资料,甚至是翻阅我之前写的文档(还有询问Ai),问题都没有很好解决,甚至自我怀疑去重新看老外的 JWT 入门课。


因为网络上已经很多人提供了解决方案,但是按照我的经历来说,我觉得更重要的是思路。因为这个问题太抽象了,所以请允许我啰嗦那么一下。

技术栈

Express 和 React。原生的 Fetch 方法(也试过 Axios)

前提

请保证,你实在找不到前后端的问题了,比如说..(这些事情你记得做了吗?)

后端:

在 App.js 入口文件有没有设置CORS和解析CookieParser的中间件

...
app.use(
    cors({
        origin: 'http://localhost:5173'(因为我的Vite的React项目),
        credentials: true(允许验证Cookie),
    })
);

...
app.use(cookieParser());
// 为什么设置这个呢?
// 因为我为了获得更高的安全性,我选择的是 JWT 通过 Cookie 传输。(详见下一篇文章说明)

上面的设置是没什么问题的(我甚至tmd还调换了代码的顺序)

前端:

之后是前端,需要保证在登陆的路由(POST你username和password那个Fetch),以及需要验证的路由(GET出数据前验证你登陆那个Fetch)

保证格式有这个:

// POST
const response = await fetch('http://localhost:3000/user/login', { // 确保这里的URL与你的后端匹配
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ username, password }),
                credentials: 'include', // <-- 确保这一行存在!
            });

// GET
const response = await fetch(`http://localhost:3000/article/${id}`, {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json', // 即使是 GET 请求,最好也指定 Content-Type
          },
          credentials: 'include', // <-- 确保这一行存在!
        });

credentials: 'include' 这一行和后端的那个差不多。这个是为了告诉浏览器,记得带上 Cookie 去 Fetch 这个接口。

中间件

我有一个中间件 (verifyLogin),用于校验用户是否登陆,否则无权查看内容,里面的开头是这样的:

const verifyLogin = (req, res, next) => {
    const authHeader = req.headers.authorization;
    const token = authHeader && authHeader.startsWith('Bearer ')
    ? authHeader.split(' ')[1]
    : req.cookies?.authToken;

    // 我的代码逻辑是先尝试从Headers里面获得Token,没有再从req(请求)的Cookie里面尝试找authToken

    if (!token) {
        console.log('未找到授权标头');
        req.user = null;
        return next();
    }
...

我访问一次需要验证的路由页面,我的后端终端就报一次 未找到授权标头。我当然不死心,我打印出赋值的 token ,结果居然是空的。


浏览器控制台

打开浏览器控制台,NetWork标签,在登陆后,明显可以看到接口返回了200,Response里面,明明就有一个 Set-Cookie .

但是在 Application(Storage) – Cookies – 点进去URL – 就是找不到有一个叫 authToken 的玩意。

我做了什么:

我对后端的 登陆路由 ,改了很久(这个很重要)

// 登陆路由
router.post('/login', async (req, res) => {
    const { username, password } = req.body;

    try {
        // 查找用户
        const user = await User.findOne({ where: { username } });
        if (!user) {
            return res.status(401).json({ error: '账号或密码错误.' });
        };

        // 验证密码
        const isPasswordValid = await bcrypt.compare(password, user.password);

        if (!isPasswordValid) {
            return res.status(401).json({ error: '账号或密码错误.' });
        };

        // 生成 Token
        const token = generateToken({
            id: user.id,
            username: user.username,
            role: user.role,
        });
        res.cookie('authToken', token, {
            httpOnly: true,          // 防止 XSS 攻击
            secure: false,            // 本地开发使用 HTTP
            sameSite: 'Lax',         // 允许跨域请求携带 Cookie
            path: '/',                   // 确保适用于所有路径
            maxAge: 14400000,   // 4小时
        });

        res.json({ message: `登录成功!欢迎回来, ${user.username}` });

    } catch(error) {
...

代码大概如上,最重要的就是:

res.cookie('authToken', token, {
            httpOnly: true,          // 防止 XSS 攻击
            secure: false,            // 本地开发使用 HTTP
            sameSite: 'Lax',         // 允许跨域请求携带 Cookie
            path: '/',                   // 确保适用于所有路径
            maxAge: 14400000,   // 4小时
        });

这里的组合一旦出错就会“爆炸”(可能是你的情绪)。


正题

好了,铺垫了那么久,我们也该进入正题了。到底为什么。

为了解决这个问题,我一共下载了 四个 不同的浏览器,最终靠着 Opera 浏览器解决了。

在 Opera 浏览器里面可以看到一个 Set-Cookie 有一个黄色的三角感叹警告标。

并且在 Application(Storage) – Cookies – 点进去URL – 能找到 authToken 这个玩意(不过是黄色的)。

当我点击开一个新的网页,总之就是网页发生变化时,这个黄色的 authToken 就直接消失了。

加上我 Chrome 莫名其妙打上了 Cookie。

我开始怀疑是不是浏览器的问题了。

是的,就是浏览器的问题,浏览器的策略不同,以至于因为你开发环境配置的
(不同的浏览器(甚至是同一个浏览器不同的版本号)它们所接受的策略组都不同)

res.cookie('authToken', token, {
            httpOnly: true,          // 防止 XSS 攻击
            secure: false,            // 本地开发使用 HTTP
            sameSite: 'Lax',         // 允许跨域请求携带 Cookie
            path: '/',                   // 确保适用于所有路径
            maxAge: 14400000,   // 4小时
        });

不同,从而打不上 authToken ,并且,除了 Opera 浏览器,其他浏览器都没让我知道发生了什么。

所以,我把配置调整成了下面这样,居然就好了

res.cookie('authToken', token, {
            httpOnly: true,          // 防止 XSS 攻击
            secure: false,            // 本地开发使用 HTTP
            sameSite: 'Lax',         // 允许跨域请求携带 Cookie
            path: '/',                   // 确保适用于所有路径
            maxAge: 14400000,   // 4小时
        });

如果你没能好,并且确定了不是代码问题,就换浏览器/关一下浏览器 Cookie过滤 看看

因为我最早就是上面的配置,未能通过,我把配置调整成这样:(依旧被拒绝)

res.cookie('authToken', token, {
            httpOnly: false,        // 允许 Javascript 获取 Cookie
            secure: false,            // 本地开发使用 HTTP
            sameSite: 'None',       // NONE
            maxAge: 14400000,   // 4小时
        });

再或者是这样的:

res.cookie('authToken', token, {
            httpOnly: true,          // 防止 XSS 攻击
            secure: true,         // 使用 HTTPS
            sameSite: 'Strict',
            path: '/',                   // 确保适用于所有路径
            maxAge: 14400000,   // 4小时
        });

无论如何,就是不行。

最终使用下面配置,成功了

res.cookie('authToken', token, {
            httpOnly: true,          // 防止 XSS 攻击
            secure: false,            // 本地开发使用 HTTP
            sameSite: 'Lax',         // 允许跨域请求携带 Cookie
            path: '/',                   // 确保适用于所有路径
            maxAge: 14400000,   // 4小时
        });

我的情况是:

3000 端口的 Express 后端。5173 端口的 React 前端。

React 前端请求 3000端口的 /login 后端登陆,获得返回的 Token

只有 HTTP,都在localhost,端口不同

最终靠上面的配置成功了。


知识拓展 – 参数代表什么

Set-Cookie 是服务器在 HTTP 响应头中设置的,用于将 Cookie 发送到客户端,浏览器会存储这些 Cookie 并在后续请求中自动带上

所以在访问这些接口的时候,要记得带上

const response = await fetch(`http://localhost:3000/article/${id}`, {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
          },
          credentials: 'include', // <-- 确保这一行存在!
    // 这样浏览器才会自动带上 Cookie
        });

res.cookie 的属性:
HttpOnly: true/false:防止客户端 JavaScript 访问 Cookie,增强安全性。

  • Secure: true/false:仅通过 HTTPS 传输 Cookie。

  • SameSite: Strict/Lax/None:控制跨站请求是否携带 Cookie(Strict、Lax 或 None)。

    • ‘Strict’:仅同站请求(Origin 相同)携带 Cookie。

    • ‘Lax’:同站请求和部分跨站请求(如 GET 导航)携带 Cookie。(Chrome 自 Chrome 84+ 起默认)

    • ‘None’:允许所有跨站请求携带 Cookie(需 secure: true)。

    • 默认值搭配:SameSite=None 必须搭配 secure: true,否则浏览器(如 Chrome)会拒绝存储。(具体看不同浏览器)

  • ExpiresMax-Age:设置 Cookie 有效期(毫秒)。

  • PathDomain:控制 Cookie 的作用范围(如’/’, ‘/user’)。

如何设置Cookie(sessionId)

const express = require('express');
const app = express();

app.post('/login', (req, res) => {
  // 假设用户登录成功,生成会话或令牌
  const user = { id: 123, username: 'example' };

  // 设置 Cookie (sessionId)
  res.cookie('sessionId', 'abc123', {
    maxAge: 24 * 60 * 60 * 1000, // 有效期 1 天
    httpOnly: true, // 防止 XSS
    secure: process.env.NODE_ENV === 'production', // 生产环境用 HTTPS
    sameSite: 'strict' // 防止 CSRF
  });

  res.json({ message: '登录成功' });
});

响应:Set-Cookie: sessionId=abc123; Max-Age=86400; HttpOnly; Secure; SameSite=Strict

如何验证Cookie(sessionId)

首先我们需要解析 Cookie:
– 手动解析:从 req.headers.cookie 中提取 Cookie 字符串,然后解析为键值对。

  • 使用中间件:许多框架(如 Express.js)提供 Cookie 解析中间件(如 cookie-parser),自动将 Cookie 解析为 req.cookies 对象。

流程是:

  1. 检查 req.cookies 中是否存在目标 Cookie(如 sessionId)。

  2. 使用 Cookie 值查询会话存储(Redis、数据库)或验证 JWT。

  3. 如果 Cookie 无效、过期或不存在,返回错误响应。

使用 cookie-parser 验证 Cookie(sessionId):

const express = require('express');
const cookieParser = require('cookie-parser');

const app = express();
app.use(cookieParser()); // 解析 Cookie

const authMiddleware = (req, res, next) => {
  const sessionId = req.cookies.sessionId; // 获取 Cookie
  if (!sessionId) {
    return res.status(401).json({ error: '未提供会话 ID' });
  }

  // 假设会话存储在 Redis 中(验证sessionId)
  redisClient.get(`session:${sessionId}`, (err, data) => {
    if (err || !data) {
      return res.status(401).json({ error: '会话无效或已过期' });
    }
    req.user = JSON.parse(data); // 将用户信息附加到 req
    next();
  });
};
  1. 基于 Session 的验证:
    • 使用 express-session 或类似库管理会话。
  • 会话数据存储在服务器端(如 Redis),客户端仅存储 sessionId。

  • 中间件检查 req.session.user 是否存在。

  1. 基于 JWT 的验证:
    • 登录时生成 JWT,存储在 Cookie 或 Authorization 头中。
  • 中间件验证 JWT 的签名和有效期。

  • 无需服务器端存储会话,适合无状态应用。

  1. 混合方式:

– 结合 Session 和 JWT:将 JWT 存储在 Cookie 中,中间件验证 JWT 并查询会话存储。

一般而言:

  • httpOnly:安全,设 true。

  • secure:开发 false,生产 true。

  • sameSite:开发 Lax,生产 None(需 secure: true)。

  • maxAge:定义有效期。

  • path:确保有效范围。


总结

总结:排除掉代码问题后,尝试调整你的res.cookie策略,更换浏览器/调整浏览器Cookie设置。再测试。


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注