Трендовые github проекты в нашем телеграм канале. Подпишись → Настраиваем защиту браузерного клиента в Spring Security
Безопасность браузерного приложения зависит не только от аутентификации. Сервер может корректно проверять пользователя по сессии или токену, однако браузер автоматически прикладывает cookie к части запросов, а политики доступа между origins определяют, какой JavaScript сможет прочитать ответ API. Эти два слоя закрывают разные риски: CSRF контролирует нежелательные действия от имени уже вошедшего пользователя, CORS ограничивает чтение ответов чужими сайтами.
Для Spring-приложения полезно сначала определить модель аутентификации и список реальных доменов фронтенда. После этого параметры CSRF и CORS становятся частью явной конфигурации, которую проще проверить при деплое.
Что происходит при CSRF
Cross-Site Request Forgery возникает, когда пользователь уже авторизован на сайте, а сторонняя страница заставляет его браузер отправить запрос на этот сайт. Например, скрытая форма или скрипт может обратиться к банковскому endpoint. Браузер распознаёт домен назначения и автоматически добавляет сохранённые для него cookie сессии. Для сервера запрос выглядит как действие авторизованного пользователя.
CSRF-защита добавляет в запрос секрет, который злоумышленник со стороннего origin не может прочитать и повторить. Spring Security сопоставляет это значение с тем, которое ожидает сервер. При отсутствии или несовпадении токена изменяющий запрос получает ответ 403 Forbidden.
Риск определяется способом передачи учётных данных:
- При Bearer JWT, который фронтенд хранит вне cookie и передаёт в заголовке
Authorization, браузер не приложит токен автоматически к запросу со сторонней страницы. В этой схеме CSRF-защита обычно не требуется. - При сессии и cookie браузер отправляет
JSESSIONIDсам. ЗапросыPOST,PUTиDELETEтребуют дополнительной проверки CSRF-токена.
Отключать CSRF стоит только после проверки этой модели. Для API на cookie-сессиях отключённая защита делает состояние пользователя доступным для подделки запросов со сторонних страниц.
CSRF-токен для REST API
В приложении с REST-фронтендом токен удобно хранить в cookie и передавать обратно отдельным заголовком. Spring Security предоставляет для этого CookieCsrfTokenRepository:
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
При первом обращении сервер создаёт случайный CSRF-токен и помещает его в cookie XSRF-TOKEN. Клиентский код читает значение cookie и добавляет его в заголовок X-XSRF-TOKEN для запросов, которые меняют данные. В запросе одновременно приходят cookie и заголовок; Spring Security сравнивает их значения.
withHttpOnlyFalse() здесь нужен именно фронтенду: JavaScript должен получить токен из cookie, чтобы сформировать заголовок. Сессионную cookie это не касается — для неё флаг HttpOnly по-прежнему нужен. Доступ к XSRF-TOKEN со страницы другого сайта ограничивает Same Origin Policy, поэтому сторонний скрипт не узнает значение, которое требуется серверу.
Типовая последовательность выглядит так:
- Сервер отвечает с
JSESSIONIDиXSRF-TOKEN. - Фронтенд отправляет
POST,PUTилиDELETEи добавляетX-XSRF-TOKEN. - Браузер добавляет cookie нужного домена.
- Spring Security сравнивает токен из cookie со значением заголовка и разрешает запрос только при совпадении.
CORS задаёт границы для чтения API
CORS расшифровывается как Cross-Origin Resource Sharing. Это набор правил, которые сервер отправляет браузеру: они определяют, какие чужие origins могут читать ответы API. Сам механизм не заменяет аутентификацию и CSRF-защиту. Его задача — не дать JavaScript с произвольного сайта получить доступ к данным ответа.
Проблема появляется при чрезмерно широких настройках. Если сервер отражает любой Origin в Access-Control-Allow-Origin и одновременно разрешает передачу cookie через Access-Control-Allow-Credentials: true, браузер сможет отдать ответ JavaScript на недоверенном сайте. Для API с пользовательскими данными список origins должен быть явным.
В Spring Security конфигурацию можно зарегистрировать как bean и подключить к цепочке фильтров:
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"http://localhost:3030",
"https://myapp.com"
));
config.setAllowedMethods(List.of(
"GET", "POST", "PUT", "DELETE", "OPTIONS"
));
config.setAllowedHeaders(List.of(
"Authorization", "Content-Type"
));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
Затем конфигурация включается в SecurityFilterChain:
http.cors(cors -> cors
.configurationSource(corsConfigurationSource())
);
setAllowedOrigins перечисляет домены фронтенда. setAllowedMethods и setAllowedHeaders ограничивают форму запросов, которую API готов принять. setMaxAge определяет время кэширования результата предварительной проверки OPTIONS в браузере.
Credentials требуют конкретных origins
Параметр setAllowCredentials(true) разрешает браузеру отправлять cookie и другие учётные данные в cross-origin запросах. Вместе с ним нельзя использовать wildcard "*" для разрешённых origins: браузер заблокирует такую комбинацию. Поэтому в конфигурации с credentials перечисляют конкретные адреса, включая локальный адрес разработки и домен production-фронтенда.
Этот список стоит пересматривать вместе с окружениями. Временный preview-домен, старый административный интерфейс или слишком широкая маска расширяют круг сайтов, которым браузер разрешит читать ответы с учётными данными пользователя.
Проверка конфигурации перед релизом
Для cookie-сессий проверьте, что изменяющие endpoint’ы принимают CSRF-токен и возвращают 403 без него. Для JWT убедитесь, что токен не записывается в cookie и клиент добавляет его только в Authorization. Для CORS сопоставьте разрешённые origins с фактически развёрнутыми фронтендами, оставьте минимальный набор методов и заголовков, а также отдельно протестируйте запросы с credentials.
Такое разделение проверок помогает сохранить предсказуемое поведение API: сессия подтверждает пользователя, CSRF-токен подтверждает намерение браузерного клиента, а CORS не открывает ответы приложения посторонним origins.