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

MERN栈项目iOS设备POST请求CSRF验证失败403问题求助

MERN栈跨域部署iOS设备CSRF验证失败403问题

我有一个MERN栈项目,当前前端部署在Vercel、后端部署在Heroku。项目在Windows和Android设备上运行完全正常,但在iPhone、Mac等iOS设备中,执行注册、登录等POST请求时,会出现CSRF validation failed ERRBADCSRF token 403错误。

此前项目在Vercel作为单项目部署时iOS设备可正常运行,但因Vercel对部分后端功能有限制,必须拆分前后端独立部署。已尝试调整sameSite配置、CORS规则、请求头设置、启用credentials、使用X-CSRF-Token作为请求头,以及添加后端域名到CORS源等方案,问题仍未解决。


相关代码

后端Index文件的CORS/CSRF配置

const env = process.env.NODE_ENV || 'development';
dotenv.config({ path: '.env' });

const app = express();

connectDB();

const db = admin.firestore();
app.use(bodyParser.json({ limit: '100mb' }));
app.use(bodyParser.urlencoded({ extended: true, limit: '100mb' }));
app.use(cookieParser());

console.log('Environment:', process.env.NODE_ENV);


app.use(cors({
  origin: process.env.FRONTEND_URL || 'http://localhost:3000',
  credentials: true,
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  allowedHeaders: 'Content-Type, Authorization, X-CSRF-Token, CSRF-TOKEN',
}));
console.log('CORS settings applied');


// Session middleware
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  store: MongoStore.create({
    mongoUrl: process.env.MONGO_URI
  }),
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    sameSite: process.env.NODE_ENV === 'production' ? 'None' : 'Lax'
  },
}));


const csrfProtection = csurf({
  cookie: {
    httpOnly: true,
    secure: true, // Set to true in production
    sameSite: process.env.NODE_ENV === 'production' ? 'None' : 'Lax',
  },
});
app.use(csrfProtection);

// Middleware to set CSRF token in a cookie for all requests
app.use((req, res, next) => {
  const csrfToken = req.csrfToken();
  console.log('Generated CSRF Token:', csrfToken); // Log generated CSRF token
  res.cookie('CSRF-TOKEN', csrfToken, {
    httpOnly: false, // Allow the client to access the cookie
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'None',
  });
  next();
});

app.get('/api/csrf-token', (req, res) => {
  const csrfToken = req.csrfToken();
  console.log('Returning CSRF Token:', csrfToken); // Log token being sent to frontend
  res.json({ csrfToken });
});


app.use((err, req, res, next) => {
  if (err.code === 'EBADCSRFTOKEN') {
    console.error('CSRF validation failed:', err);
    res.status(403).json({ error: 'Session has expired or form tampered with' });
  } else {
    next(err);
  }
});


const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 1000,
  message: 'Too many requests from this IP, please try again later',
});

app.use('/api/', limiter);

app.use(helmet()); // Applies various security headers

// Initialize Passport middleware
app.use(passport.initialize());
app.use(passport.session());
app.use(express.json());

前端api.js代码

// src/utils/api.js
import axios from 'axios';
import { BASE_URL } from '../Constants';
import { fetchCsrfToken } from './csrf';

const api = axios.create({
  baseURL: BASE_URL,
  withCredentials: true,
});

api.interceptors.request.use(
  async (config) => {
    try {
      const csrfToken = await fetchCsrfToken();
      console.log('Setting CSRF Token in request header:', csrfToken);
      config.headers['CSRF-Token'] = csrfToken;
    } catch (error) {
      console.error('Error fetching CSRF token:', error);
    }
    return config;
  },
  (error) => {
    console.error('Request setup error:', error);
    return Promise.reject(error);
  }
);

api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response) {
      console.error('Response error:', error.response.status, error.response.data);
    }
    return Promise.reject(error);
  }
);

export default api;

前端csrf.js代码

// utils/csrf.js
import { BASE_URL } from "../Constants";

export const fetchCsrfToken = async () => {
  try {
    const response = await fetch(`${BASE_URL}/csrf-token`, {
      credentials: 'include',
    });
    
    if (!response.ok) {
      throw new Error('Failed to fetch CSRF token');
    }

    const data = await response.json();
    console.log('Fetched CSRF Token on frontend:', data.csrfToken); // Log CSRF token
    return data.csrfToken;
  } catch (error) {
    console.error('Error fetching CSRF token:', error);
    throw error;
  }
};

组件handleSubmit请求代码

const handleSubmit = async (e) => {
        e.preventDefault();
    
        // Trim all input fields to remove leading/trailing spaces
        setFirstName(firstName.trim());
        setLastName(lastName.trim());
        setEmail(email.trim());
        setPhone(phone.trim());
    
        if (validateForm()) {
            setLoading(true);
            try {
                const formData = new FormData();
                formData.append('firstName', firstName.trim());
                formData.append('lastName', lastName.trim());
                formData.append('email', email.trim());
                formData.append('phone', phone.trim());
                formData.append('password', password);
                formData.append('cv', cv);
                formData.append('timezone', timezone);
    
                const response = await api.post('/instructors/register', formData);
                console.log('Registration successful:', response);
                navigate('/mentor/authentication');
            } catch (error) {
                console.error('Registration error:', error);
                setGeneralError('An error occurred. Please try again.');
            } finally {
                setLoading(false);
            }
        }
    };

解决方案

1. 统一CSRF头与Cookie命名,适配Safari大小写敏感问题

Safari对HTTP头和Cookie名称的大小写更敏感,建议统一使用X-CSRF-Token作为头名和Cookie名,避免大小写不一致导致Token匹配失败。

后端修改:

// 修改CSRF Cookie设置
res.cookie('X-CSRF-Token', csrfToken, {
  httpOnly: false,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'None',
  maxAge: 3600000, // 添加过期时间,避免Cookie被过早清理
});

// 修改CORS允许的头
app.use(cors({
  origin: process.env.FRONTEND_URL || 'http://localhost:3000',
  credentials: true,
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  allowedHeaders: 'Content-Type, Authorization, X-CSRF-Token', // 只保留统一的头名
  exposedHeaders: 'X-CSRF-Token', // 允许前端读取Cookie之外的响应头(可选)
}));

前端修改:
在api.js中设置统一的请求头:

config.headers['X-CSRF-Token'] = csrfToken;

2. 优化CSRF Token获取逻辑,直接从Cookie读取

避免每次请求都发起额外的/api/csrf-token请求,而是直接从Cookie中提取Token,减少跨域请求的不确定性,同时避免Token不匹配:

前端修改csrf.js:

// utils/csrf.js
export const fetchCsrfToken = () => {
  // 从Cookie中提取X-CSRF-Token
  const match = document.cookie.match(/X-CSRF-Token=([^;]+)/);
  if (match) {
    const token = match[1];
    console.log('Extracted CSRF Token from Cookie:', token);
    return Promise.resolve(token);
  }
  // 如果Cookie中没有,再发起请求 fallback
  return fetch(`${BASE_URL}/api/csrf-token`, {
    credentials: 'include',
  }).then(response => {
    if (!response.ok) throw new Error('Failed to fetch CSRF token');
    return response.json();
  }).then(data => {
    console.log('Fetched CSRF Token from API:', data.csrfToken);
    return data.csrfToken;
  }).catch(error => {
    console.error('Error fetching CSRF token:', error);
    throw error;
  });
};

3. 强化Session与CSRF Cookie的跨域适配

Safari对跨域Cookie的限制非常严格,必须确保Secure、SameSite=None和Domain配置正确:

后端Session配置修改:

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  store: MongoStore.create({
    mongoUrl: process.env.MONGO_URI
  }),
  cookie: {
    secure: true, // 生产环境强制设为true,Safari要求跨域Cookie必须是HTTPS
    httpOnly: true,
    sameSite: 'None',
    maxAge: 24 * 60 * 60 * 1000, // 设置合理的过期时间
    domain: process.env.COOKIE_DOMAIN, // 比如Heroku域名:'.herokuapp.com',确保子域也能读取(可选,根据实际域名调整)
  },
}));

// 修改CSRF保护的Cookie配置
const csrfProtection = csurf({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'None',
    maxAge: 3600000,
  },
});

4. 处理FormData请求的CSRF验证

当使用FormData发送请求时,csurf默认可能无法正确解析请求头,需要手动配置value函数来从请求头或FormData中提取Token:

后端修改CSRF保护配置:

const csrfProtection = csurf({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'None',
    maxAge: 3600000,
  },
  // 自定义Token提取逻辑,支持从请求头或FormData中获取
  value: (req) => {
    // 优先从请求头取
    const headerToken = req.headers['x-csrf-token'];
    if (headerToken) return headerToken;
    // 如果是FormData请求,从body中取
    if (req.body && req.body._csrf) return req.body._csrf;
    // 最后从查询参数取(可选)
    return req.query._csrf;
  }
});

如果使用FormData,可以在提交时额外添加Token字段:

// 组件handleSubmit中
formData.append('_csrf', csrfToken);

5. 调整Helmet配置,避免干扰跨域Cookie

Helmet的某些默认安全头可能会影响跨域Cookie的传递,比如Cross-Origin-Embedder-Policy或Cross-Origin-Opener-Policy,可以调整这些配置:

app.use(helmet({
  crossOriginEmbedderPolicy: false, // 禁用该策略,避免影响跨域资源
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'", process.env.FRONTEND_URL],
      // 根据实际需求添加其他规则
    },
  },
}));

6. 确认Heroku的SSL配置

确保Heroku后端使用HTTPS访问,因为Safari不允许非HTTPS环境下的跨域Secure Cookie。可以检查Heroku的SSL设置,确保已启用自动SSL证书。


内容的提问来源于stack exchange,提问作者Dawood Hamayun

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 14:50:54