MERN栈CSRF Token在iPhone浏览器失效问题求助
问题描述
我熟悉CSRF攻击及防御方案,当前开发的跨域MERN全栈应用:后端(Node.js+Express)部署在Railway.app,前端(React)部署在Netlify,使用MongoDB Atlas云数据库。此前通过HttpOnly Cookie存储token运行正常,但添加CSRF防护后,macOS浏览器可正常使用,iPhone浏览器完全失效,捕获到如下错误:
ForbiddenError: invalid csrf token at csrf (/app/node_modules/csurf/index.js:112:19) at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13) at /app/node_modules/express/lib/router/index.js:286:9 at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12) at next (/app/node_modules/express/lib/router/index.js:280:10) at urlencodedParser (/app/node_modules/body-parser/lib/types/urlencoded.js:82:7) at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13) at /app/node_modules/express/lib/router/index.js:286:9
进一步测试发现:iPhone设置中开启“允许跨网站追踪”时登录正常,关闭则触发403错误。由于是商用网站,无法要求用户修改系统设置,推测是后端server.js的配置存在问题,需要调整。
当前后端配置代码
const express = require("express"); const dotenv = require("dotenv"); const bodyParser = require("body-parser"); const cors = require("cors"); const cookieParser = require("cookie-parser"); const jwtDecode = require("jwt-decode"); const { expressjwt: jwt } = require("express-jwt"); const csrf = require("csurf"); const csrfProtection = csrf({ cookie: { secure: true, sameSite: "lax", // 尝试过'none'也无效 }, }); dotenv.config(); const app = express(); // 中间件配置 // CORS设置 const clientOrigin = process.env.NODE_ENV === "production" ? "https://MY_REAL_REACT_WEBSITE.com" : "http://localhost:3000"; const corsOptions = { origin: clientOrigin, credentials: true, methods: ["GET", "POST", "PUT", "DELETE"], optionSuccessStatus: 200, }; app.use(cors(corsOptions)); app.use(cookieParser()); const checkJwt = jwt({ secret: process.env.TOKEN_SECRET, algorithms: ["HS256"], getToken: (req) => { if (req.cookies && req.cookies.token) { return req.cookies.token; } return null; }, }).unless({ path: ["/login", "/csrfToken", "/createUser", "/logout"] }); app.use(checkJwt); app.use(bodyParser.json({ limit: "10mb" })); app.use(bodyParser.urlencoded({ extended: true, limit: "10mb" })); app.use(csrfProtection); // 问题核心所在 const port = process.env.PORT || 5001; app.use(express.json()); app.use(require("./routes/serverRoutes")); // 路由文件单独存放 const attachUser = (req, res, next) => { const token = req.cookies.token; if (!token) { return res.status(401).json({ message: "Authentication invalid" }); } const decodedToken = jwtDecode(token); if (!decodedToken) { return res .status(401) .json({ message: "There was a problem authorizing the request" }); } else { req.userId = decodedToken; next(); } }; app.use(attachUser); // 启动服务 app.listen(port, () => { console.log(`Server is running on port: ${port}`); });
解决方案
1. 调整CSRF Cookie配置适配Safari ITP限制
iPhone关闭跨网站追踪时,Safari的智能跟踪防护(ITP)会严格限制第三方Cookie,需修改CSRF Cookie的核心配置:
- 将
sameSite设为"none",必须配合secure: true(SameSite=None要求Cookie仅通过HTTPS传输) - 添加
partitioned: true属性(Safari 15.4+支持),让Cookie按站点分区存储,规避ITP的第三方Cookie限制 - 设置
httpOnly: false,因为CSRF Token需要前端读取并在请求中携带(注意:此设置仅针对CSRF Cookie,登录token的HttpOnly属性保持不变) - 生产环境需指定
domain为后端API的域名,确保跨域场景下Cookie能正确传递
修改后的CSRF配置:
const csrfProtection = csrf({ cookie: { secure: process.env.NODE_ENV === "production", // 开发环境可设为false sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", domain: process.env.NODE_ENV === "production" ? "your-backend-domain.com" : undefined, httpOnly: false, partitioned: true, maxAge: 3600000 // 可选:设置1小时过期时间 }, });
2. 修正中间件执行顺序
当前代码中JWT验证中间件在CSRF防护之前,可能导致未登录用户无法获取CSRF Token。需调整顺序,确保csrfToken接口不受JWT拦截,且CSRF防护在必要的路由前加载:
// 先加载基础中间件 app.use(cors(corsOptions)); app.use(cookieParser()); app.use(bodyParser.json({ limit: "10mb" })); app.use(bodyParser.urlencoded({ extended: true, limit: "10mb" })); app.use(express.json()); // 加载JWT验证(排除无需验证的接口) app.use(checkJwt); // 单独暴露CSRF Token接口,确保不受CSRF防护拦截 app.get("/csrfToken", (req, res) => { res.json({ csrfToken: req.csrfToken() }); }); // 加载CSRF防护,再挂载业务路由和用户信息中间件 app.use(csrfProtection); app.use(require("./routes/serverRoutes")); app.use(attachUser);
3. 前端确保正确携带CSRF Token
前端需从/csrfToken接口获取Token,并在所有非GET请求中通过X-CSRF-Token请求头携带:
// React示例:获取CSRF Token async function getCsrfToken() { const response = await fetch('https://your-backend-domain.com/csrfToken', { credentials: 'include' }); const data = await response.json(); return data.csrfToken; } // 发送POST请求示例 async function submitFormData(formData) { const csrfToken = await getCsrfToken(); const response = await fetch('https://your-backend-domain.com/api/submit', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify(formData) }); return response.json(); }
4. 添加跨域响应头增强兼容性
在后端全局添加Cross-Origin-Resource-Policy响应头,明确允许跨域资源访问:
app.use((req, res, next) => { res.header('Cross-Origin-Resource-Policy', 'cross-origin'); next(); });
核心原因总结
iPhone关闭跨网站追踪时,Safari的ITP会将跨域场景下的SameSite=Lax Cookie判定为第三方Cookie并拦截。通过将CSRF Cookie设为SameSite=None+Secure+Partitioned,配合调整中间件顺序和前端请求逻辑,可适配Safari的隐私限制,同时保持CSRF防护的有效性。
内容的提问来源于stack exchange,提问作者Rodrigo Camargo

