Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

CORS: запросы с другого сайта

Допустим, фронтенд блога пишется отдельно — на React или Vue — и во время разработки открыт на http://localhost:5173, а API работает на http://localhost:8080. Страница вызывает fetch("http://localhost:8080/api/posts"), и браузер отказывается отдавать ей ответ. Дело не в Spring: так работает защита браузера.

Что такое источник и CORS

Источник (origin) — это схема, домен и порт вместе: http://localhost:5173 и http://localhost:8080 — разные источники, хотя домен один. По умолчанию браузер не даёт скрипту страницы читать ответы с чужого источника. Иначе любой сайт, открытый у вас во вкладке, мог бы читать данные из вашей почты или банка.

CORS (Cross-Origin Resource Sharing) — правило, по которому сервер сам сообщает браузеру: «этому источнику мои ответы читать можно». Делает он это заголовком Access-Control-Allow-Origin.

Как выглядит отказ

Без настройки в консоли браузера (F12, вкладка Console) появится:

Access to fetch at 'http://localhost:8080/api/posts' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Важная деталь: сервер запрос получил и ответил 200. Блокирует браузер — он не отдаёт ответ скрипту, потому что в нём нет нужного заголовка. Поэтому curl и Postman работают, а страница — нет: они не браузеры и CORS не проверяют.

@CrossOrigin на контроллере

// src/main/java/com/example/blog/PostController.java
@RestController
@RequestMapping("/api/posts")
@CrossOrigin(origins = "http://localhost:5173")
public class PostController {
    // ...
}

Проверим, имитируя браузер заголовком Origin:

curl -i -H "Origin: http://localhost:5173" http://localhost:8080/api/posts
HTTP/1.1 200
Vary: Origin
Access-Control-Allow-Origin: http://localhost:5173
Content-Type: application/json

Заголовок Access-Control-Allow-Origin на месте — браузер отдаст ответ странице. Для запроса с Origin: http://evil.example Spring ответит 403 с текстом Invalid CORS request.

Предварительный запрос

Перед POST с JSON, PUT и DELETE браузер сначала спрашивает разрешения запросом OPTIONS — его называют preflight:

curl -i -X OPTIONS http://localhost:8080/api/posts \
  -H "Origin: http://localhost:5173" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type"
HTTP/1.1 200
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: content-type
Access-Control-Max-Age: 1800

Только получив такой ответ, браузер отправит настоящий POST. Max-Age: 1800 разрешает полчаса не переспрашивать.

Общая настройка для всего API

Ставить @CrossOrigin на каждый контроллер неудобно. Уберите аннотацию и опишите правило один раз:

// src/main/java/com/example/blog/WebConfig.java
package com.example.blog;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
public class WebConfig implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:5173")
                .allowedMethods("GET", "POST", "PUT", "DELETE");
    }
}

Теперь правило действует для всех адресов /api/**, включая картинки из прошлого урока. Метод PATCH в список не попал, и preflight для него получит 403.

Когда появится Spring Security

В шестой главе мы подключим Spring Security. Его фильтры стоят перед контроллерами, а preflight-запрос не несёт ни пароля, ни токена, поэтому получит 401 раньше, чем сработает настройка CORS. Чтобы этого не случилось, в цепочке API включают CORS:

// src/main/java/com/example/blog/SecurityConfig.java
http
        .securityMatcher("/api/**")
        .cors(Customizer.withDefaults())
        // остальные настройки цепочки

cors(Customizer.withDefaults()) берёт правила из того же WebConfig и отвечает на preflight до проверки входа.

Частая ошибка

Разрешить всё сразу: allowedOrigins("*") вместе с allowCredentials(true) (нужно, чтобы браузер отправлял cookie). Приложение запустится, но каждый запрос к /api/**, даже из curl, получит ошибку. С обработчиком IllegalArgumentException из прошлого урока это 400:

{"detail":"When allowCredentials is true, allowedOrigins cannot contain the special value \"*\" since that cannot be set on the \"Access-Control-Allow-Origin\" response header. To allow credentials to a set of origins, list them explicitly or consider using \"allowedOriginPatterns\" instead.","instance":"/api/posts","status":400,"title":"Bad Request"}

Перечисляйте конкретные адреса фронтенда. Звёздочка без cookie допустима только для открытых данных, которые и так может прочитать кто угодно.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.