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/postsHTTP/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 допустима только для открытых данных, которые и так может прочитать кто угодно.