CORS и CSRF — две аббревиатуры, с которыми Java-разработчик рано или поздно столкнется при работе с веб-приложениями. Причем обычно знакомство происходит не из учебника, а в тот момент, когда frontend внезапно получает CORS error, а попытка решить проблему заканчивается советом «поставь allowedOrigins("*")». Или когда Spring Security начинает отклонять POST-запросы из-за CSRF.
На самом деле за этими механизмами стоят вполне понятные принципы. CORS отвечает за взаимодействие браузера с ресурсами другого origin, а CSRF защищает приложение от поддельных запросов от имени уже авторизованного пользователя. Для Java-разработчика важно понимать оба механизма не только теоретически, но и знать, где они настраиваются на backend и почему некоторые очевидные решения могут быть небезопасными.
На самом деле за этими механизмами стоят вполне понятные принципы. CORS отвечает за взаимодействие браузера с ресурсами другого origin, а CSRF защищает приложение от поддельных запросов от имени уже авторизованного пользователя. Для Java-разработчика важно понимать оба механизма не только теоретически, но и знать, где они настраиваются на backend и почему некоторые очевидные решения могут быть небезопасными.
Что из себя представляет CORS
CORS, или Cross-Origin Resource Sharing, — механизм, с помощью которого сервер сообщает браузеру, какие cross-origin запросы разрешены.
Представим типичную архитектуру: frontend работает на https://frontend.example.com, а Java-приложение с REST API — на https://api.example.com. Несмотря на то что домены связаны между собой, для браузера это разные origin.
Origin определяется комбинацией протокола, домена и порта. Поэтому http://localhost:3000 и http://localhost:8080 тоже разные origin — ситуация, которая постоянно встречается при локальной разработке.
Браузер применяет Same-Origin Policy и не позволяет JavaScript бесконтрольно получать данные с других origin. CORS как раз является механизмом, который позволяет серверу сделать исключение из этого правила.
Например, Java backend может вернуть:
Представим типичную архитектуру: frontend работает на https://frontend.example.com, а Java-приложение с REST API — на https://api.example.com. Несмотря на то что домены связаны между собой, для браузера это разные origin.
Origin определяется комбинацией протокола, домена и порта. Поэтому http://localhost:3000 и http://localhost:8080 тоже разные origin — ситуация, которая постоянно встречается при локальной разработке.
Браузер применяет Same-Origin Policy и не позволяет JavaScript бесконтрольно получать данные с других origin. CORS как раз является механизмом, который позволяет серверу сделать исключение из этого правила.
Например, Java backend может вернуть:
Браузер увидит этот заголовок и разрешит frontend-коду получить ответ API.
Важно: CORS, в первую очередь, — ограничение на стороне браузера. Сервер может технически получить HTTP-запрос и даже обработать его, но браузер не обязательно позволит JavaScript-коду прочитать ответ.
Именно поэтому ошибка CORS иногда вводит в заблуждение: разработчик видит, что endpoint существует и сервер отвечает, но frontend все равно получает ошибку.
Важно: CORS, в первую очередь, — ограничение на стороне браузера. Сервер может технически получить HTTP-запрос и даже обработать его, но браузер не обязательно позволит JavaScript-коду прочитать ответ.
Именно поэтому ошибка CORS иногда вводит в заблуждение: разработчик видит, что endpoint существует и сервер отвечает, но frontend все равно получает ошибку.
Что делает preflight
Некоторые cross-origin запросы браузер не отправляет сразу. Сначала он выполняет предварительный запрос OPTIONS — так называемый preflight.
Например, frontend хочет отправить:
Например, frontend хочет отправить:
Перед этим браузер может спросить сервер:
Java-приложение должно ответить соответствующими CORS-заголовками:
После этого браузер понимает, что такой запрос разрешен, и отправляет основной запрос.
На практике именно OPTIONS часто становится источником проблем. Например, backend разрешает POST, но не обрабатывает preflight или security-фильтры блокируют OPTIONS. В результате разработчик видит ошибку на frontend, хотя проблема находится в конфигурации Java-приложения.
На практике именно OPTIONS часто становится источником проблем. Например, backend разрешает POST, но не обрабатывает preflight или security-фильтры блокируют OPTIONS. В результате разработчик видит ошибку на frontend, хотя проблема находится в конфигурации Java-приложения.
Как настроить CORS в Spring
В Spring Boot CORS можно настроить через CorsConfigurationSource. Например, можно создать бин, в котором указать разрешенный origin, HTTP-методы и заголовки:
Затем CORS нужно включить в конфигурации Spring Security:
Если CorsConfigurationSource зарегистрирован как бин, Spring Security сможет использовать его конфигурацию автоматически. В результате backend будет разрешать cross-origin запросы только с указанного origin и только с теми методами и заголовками, которые мы явно разрешили.
Конкретный набор разрешений зависит от приложения. Например, если frontend отправляет PATCH или использует дополнительные заголовки, их тоже нужно добавить в конфигурацию.
Здесь стоит запомнить главное правило: не стоит разрешать все origin просто ради того, чтобы исчезла ошибка CORS.
Например, разрешение:
Конкретный набор разрешений зависит от приложения. Например, если frontend отправляет PATCH или использует дополнительные заголовки, их тоже нужно добавить в конфигурацию.
Здесь стоит запомнить главное правило: не стоит разрешать все origin просто ради того, чтобы исчезла ошибка CORS.
Например, разрешение:
может быть подходящим для действительно публичного API, которому не нужны пользовательские credentials. Но если API работает с авторизованными пользователями и cookies, origin лучше указывать явно.
Например, frontend может отправлять запрос вместе с credentials:
Например, frontend может отправлять запрос вместе с credentials:
В этом случае сервер должен явно разрешить конкретный origin и credentials:
Поэтому комбинация Access-Control-Allow-Origin: * и Access-Control-Allow-Credentials: true для такого сценария не подходит.
Что из себя представляет CSRF
Если CORS отвечает за cross-origin взаимодействие, то CSRF — это уже про безопасность действий пользователя.
CSRF расшифровывается как Cross-Site Request Forgery. Суть атаки в том, что злоумышленник пытается заставить браузер авторизованного пользователя выполнить действие на другом сайте.
Представим, что пользователь авторизовался в интернет-магазине, а идентификатор его сессии хранится в cookie. Браузер автоматически отправляет эту cookie вместе с запросами к магазину.
Пользователь затем открывает сторонний сайт. Этот сайт пытается отправить запрос в интернет-магазин. Если сервер просто видит валидную cookie и не проверяет, откуда пришел запрос, он может воспринять его как действие самого пользователя.
Именно эту проблему и решает CSRF-защита.
Один из классических способов — CSRF-токен. Сервер генерирует случайное значение, которое клиент должен передать вместе с изменяющим состояние запросом. Сторонний сайт не должен иметь возможности получить этот токен, поэтому не сможет корректно сформировать запрос.
Например:
CSRF расшифровывается как Cross-Site Request Forgery. Суть атаки в том, что злоумышленник пытается заставить браузер авторизованного пользователя выполнить действие на другом сайте.
Представим, что пользователь авторизовался в интернет-магазине, а идентификатор его сессии хранится в cookie. Браузер автоматически отправляет эту cookie вместе с запросами к магазину.
Пользователь затем открывает сторонний сайт. Этот сайт пытается отправить запрос в интернет-магазин. Если сервер просто видит валидную cookie и не проверяет, откуда пришел запрос, он может воспринять его как действие самого пользователя.
Именно эту проблему и решает CSRF-защита.
Один из классических способов — CSRF-токен. Сервер генерирует случайное значение, которое клиент должен передать вместе с изменяющим состояние запросом. Сторонний сайт не должен иметь возможности получить этот токен, поэтому не сможет корректно сформировать запрос.
Например:
Backend проверяет X-CSRF-Token. Если значение отсутствует или не совпадает с ожидаемым, запрос отклоняется.
А где здесь Spring Security?
Если вы работали со Spring Security, то наверняка сталкивались с CSRF еще до того, как начали специально его изучать.
Для приложений, использующих cookie-based authentication, CSRF-защита может быть важной частью конфигурации. Spring Security по умолчанию учитывает CSRF для запросов, которые могут изменять состояние приложения.
Именно поэтому разработчик может внезапно получить ответ вроде 403 Forbidden при отправке POST, PUT или DELETE и обнаружить, что GET при этом работает нормально.
Распространенная реакция — просто написать:
Для приложений, использующих cookie-based authentication, CSRF-защита может быть важной частью конфигурации. Spring Security по умолчанию учитывает CSRF для запросов, которые могут изменять состояние приложения.
Именно поэтому разработчик может внезапно получить ответ вроде 403 Forbidden при отправке POST, PUT или DELETE и обнаружить, что GET при этом работает нормально.
Распространенная реакция — просто написать:
и продолжить разработку.
Но csrf.disable() — не «фикс ошибки», а изменение модели безопасности приложения. Отключать CSRF можно только понимая, почему защита действительно не нужна.
Например, многие REST API используют stateless-аутентификацию через токен в заголовке:
Но csrf.disable() — не «фикс ошибки», а изменение модели безопасности приложения. Отключать CSRF можно только понимая, почему защита действительно не нужна.
Например, многие REST API используют stateless-аутентификацию через токен в заголовке:
Если браузер не прикладывает этот токен автоматически к cross-site запросам, классическая CSRF-атака работает иначе, чем в приложении с сессионными cookie.
Поэтому решение зависит не от того, называется ли приложение REST API и используется ли JWT, а от того, как именно устроена аутентификация и где хранятся credentials.
Поэтому решение зависит не от того, называется ли приложение REST API и используется ли JWT, а от того, как именно устроена аутентификация и где хранятся credentials.
JWT не означает автоматически «CSRF не нужен»
Это распространенное упрощение.
JWT можно хранить, например, в cookie. В таком случае браузер будет автоматически отправлять cookie, и CSRF снова становится актуальной угрозой.
Другой вариант — frontend получает токен и самостоятельно добавляет его в Authorization:
JWT можно хранить, например, в cookie. В таком случае браузер будет автоматически отправлять cookie, и CSRF снова становится актуальной угрозой.
Другой вариант — frontend получает токен и самостоятельно добавляет его в Authorization:
В этом случае сторонний сайт не может просто заставить браузер добавить произвольный Authorization header с вашим токеном.
Но это не означает, что безопасность заканчивается на CSRF. Остаются другие вопросы: XSS, срок жизни токена, хранение credentials, CORS, HTTPS и общая архитектура аутентификации.
Поэтому правильнее смотреть не на модное слово «JWT», а на конкретный механизм передачи учетных данных.
Но это не означает, что безопасность заканчивается на CSRF. Остаются другие вопросы: XSS, срок жизни токена, хранение credentials, CORS, HTTPS и общая архитектура аутентификации.
Поэтому правильнее смотреть не на модное слово «JWT», а на конкретный механизм передачи учетных данных.
Какую роль играют SameSite, Secure и HttpOnly
Если приложение использует cookie для сессии или аутентификации, Java-разработчику также важно знать основные атрибуты cookie.
Это не отменяет необходимости проектировать защиту комплексно, но показывает важную вещь: современная веб-безопасность строится не на одном переключателе в Spring Security.
- HttpOnly запрещает JavaScript напрямую читать cookie. Это полезно для защиты от некоторых сценариев кражи сессионных данных при XSS.
- Secure заставляет браузер отправлять cookie только по HTTPS.
- А SameSite определяет, когда cookie может отправляться в cross-site сценариях. Например, SameSite=Lax или SameSite=Strict могут существенно снизить риск CSRF, потому что браузер не будет автоматически прикладывать cookie во всех возможных cross-site запросах.
Это не отменяет необходимости проектировать защиту комплексно, но показывает важную вещь: современная веб-безопасность строится не на одном переключателе в Spring Security.
CORS и CSRF часто путают
У этих механизмов разные задачи.
Представим frontend на localhost:3000 и Java API на localhost:8080. Ошибка CORS означает, что браузер не разрешает frontend-коду получить ответ API согласно политике CORS.
А если Spring Security отвечает 403 из-за отсутствующего CSRF-токена, проблема уже совершенно другая: сервер ожидает подтверждение того, что изменяющий состояние запрос сформирован доверенным клиентом.
Поэтому CORS нельзя использовать как замену CSRF, а CSRF — как способ исправить CORS.
- CORS отвечает за то, может ли frontend с одного origin получить доступ к ресурсу другого origin через браузер.
- CSRF защищает от выполнения нежелательных действий с использованием уже существующей авторизации пользователя.
Представим frontend на localhost:3000 и Java API на localhost:8080. Ошибка CORS означает, что браузер не разрешает frontend-коду получить ответ API согласно политике CORS.
А если Spring Security отвечает 403 из-за отсутствующего CSRF-токена, проблема уже совершенно другая: сервер ожидает подтверждение того, что изменяющий состояние запрос сформирован доверенным клиентом.
Поэтому CORS нельзя использовать как замену CSRF, а CSRF — как способ исправить CORS.
Что происходит в реальном Java-проекте
Типичный сценарий может выглядеть так: frontend отправляет POST-запрос на Spring Boot API, браузер сначала выполняет preflight OPTIONS, backend должен корректно обработать CORS, затем приходит основной запрос. Если приложение использует cookie-based authentication, вместе с ним могут отправляться credentials, а Spring Security дополнительно проверяет CSRF-токен.
Получается сразу несколько уровней, которые легко смешать в одну проблему: браузер проверяет origin и CORS, Spring Security применяет свои security-фильтры, а приложение дополнительно проверяет аутентификацию и права пользователя.
Поэтому при ошибке важно не начинать с хаотичного отключения защитных механизмов, а определить, на каком именно уровне запрос был отклонен.
Получается сразу несколько уровней, которые легко смешать в одну проблему: браузер проверяет origin и CORS, Spring Security применяет свои security-фильтры, а приложение дополнительно проверяет аутентификацию и права пользователя.
Поэтому при ошибке важно не начинать с хаотичного отключения защитных механизмов, а определить, на каком именно уровне запрос был отклонен.
В итоге важно не путать CORS и CSRF: они решают разные задачи и требуют разного подхода к настройке. Для Java-разработчика главное — понимать, что происходит с запросом на каждом этапе, а не отключать защитные механизмы, не разобравшись в причине ошибки.
Хотите узнать больше? Изучите другие статьи из разделов
Хотите узнать больше? Изучите другие статьи из разделов