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

MERN栈CSRF Token在iPhone浏览器失效问题求助

跨域MERN应用CSRF防护在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 10:22:02