为了解决(搞明白)这个问题,实在花了我超级长的时间,由于我是熬夜写代码,一写就是很长一段时间,甚至一两天。以至于我一直盯着我的前后端代码,怎么都看不出来个所以然。
即便查阅了大量资料,甚至是翻阅我之前写的文档(还有询问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)会拒绝存储。(具体看不同浏览器)
-
Expires
或Max-Age
:设置 Cookie 有效期(毫秒)。 -
Path
和Domain
:控制 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 对象。
流程是:
- 检查 req.cookies 中是否存在目标 Cookie(如 sessionId)。
-
使用 Cookie 值查询会话存储(Redis、数据库)或验证 JWT。
-
如果 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();
});
};
- 基于 Session 的验证:
- 使用 express-session 或类似库管理会话。
- 会话数据存储在服务器端(如 Redis),客户端仅存储 sessionId。
-
中间件检查 req.session.user 是否存在。
- 基于 JWT 的验证:
- 登录时生成 JWT,存储在 Cookie 或 Authorization 头中。
-
中间件验证 JWT 的签名和有效期。
-
无需服务器端存储会话,适合无状态应用。
- 混合方式:
– 结合 Session 和 JWT:将 JWT 存储在 Cookie 中,中间件验证 JWT 并查询会话存储。
一般而言:
- httpOnly:安全,设 true。
-
secure:开发 false,生产 true。
-
sameSite:开发 Lax,生产 None(需 secure: true)。
-
maxAge:定义有效期。
-
path:确保有效范围。
总结
总结:排除掉代码问题后,尝试调整你的res.cookie策略,更换浏览器/调整浏览器Cookie设置。再测试。
发表回复