我有一个 MERN Stack 项目(最初在 Vercel 上部署为两个不同的项目,但现在 Vercel 部署在前端,后端部署在 Heroku 上)。
整个应用程序运行得非常好,除了在 iOS 设备(如 iPhone 或 Mac)中,如果我执行注册或登录等发布请求,我会得到一个
csrf 验证失败 ERRBADCSRF 令牌 403 错误。它在 Windows 或 Android 上运行良好。
相关代码如下:
索引 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());
前端的 auth.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
// utils/csrf.js
import { BASE_URL } from "../Constants";
export const fetchCsrfToken = async () => {
try {
const response = await fetch(`${BASE_URL}/csrf-token`, { // Correct usage of template literals
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);
}
}
};
我已经尝试了几乎所有我能想到的东西,包括搞乱sameSite配置、cors配置,包括标头、使用凭据、使用X-CSRF令牌作为前端的标头。我也尝试过在 Cors Origin 上添加后端域。
我还尝试检查我的域中的前端 URL。奇怪的是,当我尝试将其作为单个项目部署在 Vercel 上时(前端和后端均部署为单个项目),它运行得非常好。尽管它也适用于 iPhone,但我无法真正使用它,因为 Vercel 对某些后端功能有一些限制。我必须在不同的域上以不同的方式设置后端。
我非常需要帮助!
我似乎使用了两个不同的域。对于后端,它需要是相同的域。相反,您只需要添加两个不同的子域。
否则 iOS 设备会认为它是第三方 cookie 并阻止它。
因此,只需确保您具有相同的域设置,并为前端和后端设置两个不同的子域。