← Назад к вопросам

Как разрешить клиенту выходить в другой домен?

2.3 Middle🔥 181 комментариев
#Soft Skills и рабочие процессы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как разрешить клиенту обращаться к другому домену (CORS)

Это вопрос о Cross-Origin Resource Sharing (CORS) — механизме безопасности браузера, который контролирует доступ к ресурсам на других доменах. Это один из самых частых багов в веб-разработке.

Проблема: Same-Origin Policy

По умолчанию браузер блокирует запросы к другим доменам по соображениям безопасности:

// Приложение на https://app.com:3000
// Пытаемся обратиться к API на https://api.com:8000

fetch('https://api.com:8000/api/users')
  .then(res => res.json())
  .catch(err => console.error(err));

// Результат: CORS Error
// Access to XMLHttpRequest at 'https://api.com:8000/api/users'
// from origin 'https://app.com:3000' has been blocked by CORS policy

Что такое Origin

Origin состоит из трех частей: протокол + домен + порт

// Одинаковые origins (разрешены)
https://example.com:3000  === https://example.com:3000  // ОК

// Разные origins (заблокированы CORS)
https://example.com      !== https://api.example.com   // Разные домены
https://example.com:3000 !== https://example.com:8000  // Разные порты
http://example.com       !== https://example.com       // Разные протоколы

Решение: CORS на сервере

Сервер должен явно разрешить кроссдоменные запросы, отправляя специальные заголовки:

// На СЕРВЕРЕ (например, Express.js)
const express = require('express');
const cors = require('cors');
const app = express();

// Простой способ: разрешить всем
app.use(cors());

// Или с конфигурацией
app.use(cors({
  origin: 'https://app.com',        // Разрешить конкретный домен
  methods: ['GET', 'POST', 'PUT'],   // Разрешить методы
  credentials: true,                 // Разрешить cookies
  optionsSuccessStatus: 200          // Для IE11
}));

// Или разрешить несколько доменов
const allowedOrigins = ['https://app.com', 'https://admin.com'];
app.use(cors({
  origin: (origin, callback) => {
    if (allowedOrigins.includes(origin) || !origin) {
      callback(null, true);
    } else {
      callback(new Error('CORS not allowed'));
    }
  }
}));

Вручную установить CORS заголовки

// На СЕРВЕРЕ без библиотек
app.use((req, res, next) => {
  // Разрешить конкретный origin
  res.header('Access-Control-Allow-Origin', 'https://app.com');
  
  // Или разрешить любому (небезопасно)
  res.header('Access-Control-Allow-Origin', '*');
  
  // Разрешить методы
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  
  // Разрешить заголовки
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  
  // Разрешить credentials (cookies, auth headers)
  res.header('Access-Control-Allow-Credentials', 'true');
  
  // Кешировать preflight запрос на 24 часа
  res.header('Access-Control-Max-Age', '86400');
  
  // Обработать preflight запрос
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  
  next();
});

Preflight запросы (OPTIONS)

Для сложных запросов браузер сначала отправляет OPTIONS запрос:

// На КЛИЕНТЕ: запрос с кастомным заголовком
fetch('https://api.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value'  // Кастомный заголовок
  },
  body: JSON.stringify({ data: 'test' })
});

// Браузер отправит ДВА запроса:
// 1. OPTIONS https://api.com/data (preflight)
//    Вопрос: можно ли отправить POST с этими заголовками?
//    
// 2. POST https://api.com/data (если OPTIONS вернул 200)

// ПРОСТОЙ запрос (без preflight)
fetch('https://api.com/data', {
  method: 'GET',  // Или POST с application/x-www-form-urlencoded
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
});
// Браузер отправит ТОЛЬКО основной запрос

Когда требуется preflight

// ТРЕБУЕТ preflight (будет OPTIONS перед GET)
fetch('https://api.com/data', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer token'  // Кастомный заголовок
  }
});

// ТРЕБУЕТ preflight (будет OPTIONS перед POST)
fetch('https://api.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'  // Не form-data
  },
  body: JSON.stringify({ test: true })
});

// НЕ требует preflight (simple request)
fetch('https://api.com/data', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
});

На клиенте: обработка CORS ошибок

// КЛИЕНТ: обращение к API на другом домене
fetch('https://api.com/users', {
  method: 'GET',
  credentials: 'include',  // Отправить cookies если есть
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + token
  }
})
  .then(res => {
    if (!res.ok) throw new Error(`HTTP error status: ${res.status}`);
    return res.json();
  })
  .catch(err => {
    console.error('CORS Error или Network Error:', err.message);
    // Примечание: детали CORS ошибки скрыты для безопасности
    // Вы видите просто: "Failed to fetch"
  });

// С axios
import axios from 'axios';

const instance = axios.create({
  baseURL: 'https://api.com',
  withCredentials: true  // Важно для cookies
});

instance.get('/users')
  .then(res => console.log(res.data))
  .catch(err => console.error(err.message));

Практический пример: работа с разными доменами

// BACKEND (FastAPI Python)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.com", "http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/api/users")
async def get_users():
    return {"users": [{"id": 1, "name": "Alice"}]}

// FRONTEND (React на другом домене)
function App() {
  const [users, setUsers] = useState([]);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetch('https://api.com/api/users', {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      },
      credentials: 'include'  // Отправить cookies
    })
      .then(res => res.json())
      .then(data => setUsers(data.users))
      .catch(err => setError(err.message));
  }, []);
  
  return (
    <div>
      {error && <p>Error: {error}</p>}
      {users.map(user => <p key={user.id}>{user.name}</p>)}
    </div>
  );
}

Альтернативные решения

1. JSONP (устаревший метод)

// Работает через <script>, но небезопасен
function handleResponse(data) {
  console.log(data);
}

// Сервер вернет: handleResponse({users: [...]})
const script = document.createElement('script');
script.src = 'https://api.com/users?callback=handleResponse';
document.body.appendChild(script);

2. Прокси сервер (на своем домене)

// На https://app.com/api/* прокси запросы к https://api.com/*
// КЛИЕНТ обращается к своему домену
fetch('/api/users')  // -> запрос к https://app.com/api/users
  .then(res => res.json());

// На сервере настраивается перенаправление
// /api/* -> https://api.com/*

3. Same-Site API (на одном домене)

// Вместо обращения к api.com, используй app.com/api
// Это самый безопасный способ
fetch('/api/users')  // На том же домене
  .then(res => res.json());

Выводы

  1. CORS защищает браузер от несанкционированного доступа к данным
  2. Сервер должен разрешить через заголовки Access-Control-*
  3. Preflight (OPTIONS) отправляется автоматически для сложных запросов
  4. credentials: true нужен для отправки cookies/auth
  5. Never use * в production для Access-Control-Allow-Origin с credentials
  6. Лучший способ — использовать прокси на своем домене
  7. Заголовок Authorization требует preflight запроса
Как разрешить клиенту выходить в другой домен? | PrepBro