JavaRush /Курсы /Spring Security /CSRF‑токен: missing, invalid, устаревший

CSRF‑токен: missing, invalid, устаревший

Spring Security
11 уровень , 4 лекция
Открыта

1. CSRF‑403: «рандомный» отказ

Когда вы впервые ловите CSRF‑отказ, он обычно выглядит крайне несправедливо: «Я же залогинен, я же USER, почему мне запрещают обновить свой же профиль?!». В этой лекции мы сделаем важный психологический шаг: научимся видеть в 403 не “Spring сломался”, а “Spring отработал по плану”. CSRF‑проверка — это отдельный слой безопасности, который не спорит с ролями и не заменяет аутентификацию, а отвечает на другой вопрос: корректен ли сам browser‑сценарий запроса.

Если упростить до бытовой аналогии, роль и логин — это «кто вы и что вам вообще разрешено», а CSRF — это «вы точно сами нажали кнопку “Удалить”, а не кто-то подсунул вам её через хитрую вкладку?». Поэтому CSRF‑сбой часто случается именно тогда, когда аутентификация уже есть, и тем сильнее кажется, что “всё должно работать”.

Нормальная реакция сервера при CSRF‑сбое — остановить запрос раньше, чем он попадёт в ваш контроллер. Отсюда и ощущение «ничего не исполняется»: вы добавили лог в метод, а лог не появляется; вы ожидаете, что валидация DTO сработает, а она даже не началась; вы думаете, что у вас баг в сервисе, а сервис даже не видел этот запрос. Это и есть «нормально», потому что CSRF — защита на границе, до бизнес‑кода.

2. Где останавливается запрос: CsrfFilter

Если в голове нет картинки «где именно Spring Security принимает решение», начинаются ритуальные танцы: подкрутить контроллер, поменять DTO, добавить @Valid, сделать ещё один @RequestMapping… а проблема остаётся. Поэтому сейчас мы фиксируем простой факт: CSRF‑проверка живёт в security‑слое и выполняется фильтром до того, как управление попадёт в Spring MVC. То есть до @Controller, до @RestController и до вашей бизнес‑логики — именно поэтому CSRF‑сбой выглядит как “контроллер не вызывается”.

В упрощённом виде часть жизненного цикла запроса можно представить так:

flowchart TD
    %% CSRF ломает запрос на уровне фильтров, до MVC и контроллера
    A["HTTP request"] --> B["Security filter chain"]
    B --> C["CsrfFilter: надо ли проверять?"]
    C -->|Safe method: GET/HEAD/OPTIONS...| D["Идём дальше"]
    C -->|State-changing: POST/PATCH/PUT/DELETE| E{"Есть token?"}
    E -->|нет| F["Access denied -> 403"]
    E -->|да| G{"Совпадает с ожидаемым?"}
    G -->|нет| F
    G -->|да| H["Идём дальше по цепочке"]
    H --> I["DispatcherServlet -> Controller"]

По умолчанию Spring Security вообще не запускает эту проверку для GET, HEAD, TRACE и OPTIONS; она интересуется unsafe methods. Поэтому первый диагностический вопрос всегда очень приземлённый: какой HTTP‑метод реально ушёл на сервер.

Самый простой способ почувствовать это руками — поставить «маячок» в контроллер и убедиться, что при CSRF‑отказе этот маячок не срабатывает.

Вот минимальный пример для нашего проекта (профиль текущего пользователя). Обратите внимание: пример маленький, но очень показательный.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // Важно: контроллер может быть "идеально написан", но CSRF остановит запрос раньше
class MeProfileController {

    private static final Logger log = LoggerFactory.getLogger(MeProfileController.class);

    @PatchMapping("/api/me/profile") // PATCH = state-changing -> CSRF проверка будет обязательной
    public void updateProfile() {
        // "Маячок": если CSRF не прошёл, эта строка вообще не выполнится
        log.info("updateProfile() reached"); // не появится, если CSRF проверка не прошла
    }
}

Если вы отправите PATCH /api/me/profile без корректного CSRF‑токена, то log.info(...) не выполнится, потому что CsrfFilter остановит запрос раньше. Это важнейшая мысль для диагностики: когда вы ловите CSRF‑сбой, вы можете час искать ошибку в контроллере, но контроллер в это время вообще не участвует.

И вот тут появляется частая «ложная улика»: многие новички видят 403 и думают, что это про роли (hasRole, hasAuthority). Но CSRF‑отказ — это тоже 403, просто причина другая. Пока мы не пришли к единому JSON error contract (это будет гораздо позже), самый честный инструмент — правильно прочитать ситуацию через механику фильтра: request не дошёл до MVC, значит причина почти наверняка в security boundary.

3. Missing/invalid/«устаревший» токен

На практике под словом «CSRF не работает» прячется не одна, а несколько разных ситуаций. Они похожи внешне (все приводят к отказу), но лечатся разными действиями. Поэтому дальше мы договоримся о трёх терминах, которые очень удобны именно для отладки: missing token, invalid token и «устаревший» token. Последний термин не всегда звучит официально в логах, но как ментальная модель он великолепен: он объясняет, почему “вчера работало, а сегодня вдруг перестало”, хотя вы ничего не меняли в коде.

Для удобства сведём это в маленькую таблицу. Она не про академическую точность, а про быстрый инженерный мозг в реальной ситуации.

Сценарий Что это означает по-человечески Что обычно не так в запросе Самая частая причина
Missing token Токен вообще не пришёл Нет заголовка/параметра с токеном Запрос из Postman/curl без CSRF‑поля; JS‑клиент не добавляет токен
Invalid token Токен пришёл, но не совпал с ожидаемым Токен не от той сессии, “битый”, обрезанный, из другого окна Вы взяли токен в одной сессии, а отправили с другой; не отправили session cookie; токен скопирован с лишним пробелом
«Устаревший» token Вы отправляете “вчерашний” токен, а сервер ждёт другой Формально тоже invalid, но причина — рассинхронизация состояния Истекла HttpSession, сервер создал новую; вы перелогинились; браузер обновил состояние, а клиент продолжил слать старый токен

Теперь разберём эти случаи человеческим языком.

Missing token — самый «чистый» случай. Сервер говорит: «Для этого метода нужна CSRF‑проверка, но вы вообще не принесли второй ключ». Это как прийти в клуб по списку гостей: вы реально в списке, но забыли паспорт. Клуб не спорит, что вы “наверное тот самый”, но правила есть правила.

Invalid token — более коварный. Вы токен принесли, но сервер смотрит на него и говорит: «Хорошая попытка, но это не тот токен, который я жду для текущей сессии». И вот тут начинается магия session‑модели: ожидаемое значение живёт на стороне сервера (обычно в HttpSession). Если запрос приходит без правильной session cookie, сервер, скорее всего, видит другую сессию (или новую) — и ожидает другой токен.

«Устаревший» token — практически тот же invalid, но психологически важнее. Он объясняет ситуации вида «я оставил вкладку на ночь, утром нажал “Сохранить профиль”, и всё умерло». За ночь HttpSession могла истечь. Браузер при этом честно продолжает слать cookie JSESSIONID (потому что “у меня же есть cookie”), но сервер по этому ID больше не находит живую сессию (потому что “я уже её забыл”). Итог: сервер создаёт новую сессию и новый ожидаемый CSRF‑токен, а клиент отправляет старое значение, сохранённое где-то в UI‑состоянии или в переменной. Формально это invalid, но по сути — “мы рассинхронизировались”.

4. Диагностика CSRF: 4 проверки

Когда вы видите CSRF‑отказ, очень хочется действовать по эмоциям: отключить защиту, «попробовать другое», сделать endpoint GET /delete (только не надо, пожалуйста). Вместо этого полезно иметь короткий диагностический алгоритм, который занимает минуту и не требует шаманства. Сейчас мы соберём именно такой алгоритм: четыре проверки, которые почти всегда выводят на причину. Это будет ваш «чек‑лист спокойствия», когда Spring Security снова решит вас воспитывать.

Представим, что у вас есть проблемный запрос (например, PATCH /api/me/profile), который внезапно получает 403. Первое, что вы делаете, — не лезете в сервисы, а проверяете следующее.

Проверка №1 — это действительно state-changing метод? Если вы случайно вызываете GET (или у вас endpoint спроектирован как “удаление через GET”), вы ломаете базовую модель: CSRF‑механизм заточен под то, что GET остаётся чтением. В обычной конфигурации Spring Security для GET CSRF‑проверки нет, а для PATCH/POST/DELETE она есть. Если у вас “удаление через GET”, вы создаёте ситуацию, где браузер может выполнить опасное действие без CSRF‑защиты просто по ссылке — и потом ещё удивляться, что «почему же CSRF меня не спас».

Проверка №2 — есть ли у запроса правильная сессия (cookie JSESSIONID)? В браузере это почти всегда «да», потому что браузер отправляет cookies автоматически. В Postman/curl — очень часто «нет» (пока вы явно не сохранили cookie из логина и не прикрепили её к следующему запросу). А без правильной сессии вы практически гарантированно получите invalid token, даже если “токен где-то лежит”.

Проверка №3 — принесли ли вы CSRF‑токен в нужном месте? В Spring Security у токена есть «имя параметра» и «имя заголовка» (мы видели это через CsrfToken). В типичном сценарии вам нужно либо добавить параметр _csrf (в form submit), либо заголовок вроде X-CSRF-TOKEN. Если токена нет вообще, это missing token. И тут важно не путать “я не знаю, куда его класть” с “значит, надо отключить CSRF”. Нормальная реакция — научиться корректно его передавать именно в вашем клиенте.

Проверка №4 — токен соответствует именно этой сессии? Это пункт, где чаще всего всплывает “устаревший” token: вы используете значение, которое было сгенерировано для другой HttpSession. Классический случай — вы залогинились заново (или сессия истекла), но клиент продолжил слать старое значение. В результате сервер ждёт новое, а приходит старое: invalid.

Чтобы не держать всё это в голове, удобно представить диагностику как простую блок‑схему:

flowchart TD
    %% Быстрый чек-лист: метод -> cookie -> token -> соответствие сессии
    A["Получили 403 на POST/PATCH/DELETE"] --> B{"Метод действительно state-changing?"}
    B -->|нет| C["Проверить дизайн endpoint'а и HTTP method"]
    B -->|да| D{"Сессия (JSESSIONID) отправляется?"}
    D -->|нет| E["Клиент не сохраняет cookie -> будет invalid"]
    D -->|да| F{"CSRF token отправлен?"}
    F -->|нет| G["Missing token"]
    F -->|да| H{"Token соответствует этой сессии?"}
    H -->|нет| I["Invalid/устаревший token -> рассинхронизация"]
    H -->|да| J["Ищем другие причины 403 (правила доступа)"]

Обратите внимание на последнюю ветку: “если всё совпало — ищем другие причины 403”. Это важно, чтобы не превращать CSRF в козла отпущения. Иногда 403 действительно из-за ролей. Но пока вы не проверили “сессия + токен”, спорить о ролях — как чинить двигатель, не проверив, что в машине вообще есть бензин.

5. CSRF‑сбои на endpoint’ах

Теория всегда звучит убедительно, но окончательно мозг верит только тому, что он увидел на своём же проекте. Поэтому сейчас привяжем missing/invalid/«устаревший» к нашим реальным endpoint’ам Secure Content Platform API. Мы не будем изобретать искусственные примеры — возьмём ровно те операции, которые студент уже видит в проекте: update профиля, update черновика, submit черновика и delete. Цель простая: чтобы вы могли по одному взгляду на запрос сказать: «ага, тут точно будет CSRF‑проверка, и вот где она сломалась».

Представим обычный локальный диагностический сценарий: вы залогинились через браузер (form login), а state‑changing запрос добиваете через curl. Cookie JSESSIONID смотрите в DevTools, а token берёте из временного local-only /debug/csrf, который держите именно для ручной отладки. Это не продуктовый способ “работать с CSRF”, а просто увеличительное стекло для диагностики. В браузере всё это обычно собирается само, а в curl вы — как взрослый человек — сами отвечаете за то, чтобы принести нужные кусочки запроса.

Missing token на PATCH /api/me/profile

Если вы сделали запрос, отправив cookie сессии, но забыли токен, это классический missing token. Пример (placeholder’ы намеренно условные):

# Есть cookie сессии (аутентификация "как бы" есть)
# Но CSRF token не отправлен -> ожидаем отказ
curl -i -X PATCH \
  -b "JSESSIONID=abc123" \
  -H "Content-Type: application/json" \
  -d '{"displayName":"Neo"}' \
  http://localhost:8080/api/me/profile

Смысл этого примера не в том, чтобы вы запомнили синтаксис curl, а в том, чтобы вы увидели: “я аутентифицирован (есть cookie), но без CSRF‑токена state‑changing операция не проходит”.

Invalid token на DELETE /api/drafts/{id}

Теперь усложним чуть-чуть: вы решили “ну ок, нужен токен”, добавили заголовок, но забыли cookie. В browser‑мире cookie “едет” автоматически, а в curl — нет. Сервер увидит новый/другой session context и будет ожидать другой токен.

# CSRF token отправили...
# ...но cookie сессии не отправили -> token почти наверняка invalid
curl -i -X DELETE \
  -H "X-CSRF-TOKEN: token-from-somewhere" \
  http://localhost:8080/api/drafts/10

Почему это invalid? Потому что ожидаемый токен сидит в HttpSession, а вы не передали идентификатор сессии. Сервер создал новую сессию (или вообще работает без неё до первого обращения) и сравнил ваш токен с тем, что у него в памяти. Совпадение было бы чудом. А чудеса мы оставим для новогодних фильмов, не для backend’а.

«Устаревший» token на POST /api/drafts/{id}/submit

Самая неприятная ситуация по ощущениям выглядит так: “я всё отправляю правильно, но иногда не работает”. Обычно это рассинхронизация по времени. Вы получили токен, потом долго ничего не делали, сессия истекла, а вы отправили “старое значение”.

В чистом виде в curl это выглядит так же, как invalid token (потому что технически это он и есть). Но в жизни это часто проявляется на настоящей вкладке: вы открыли страницу, токен где-то подставляется в форму или хранится в JS‑переменной, потом вы ушли пить чай (или на созвон, это бывает), а когда вернулись — сессии уже нет. И ваш первый submit или update падает.

Если вы хотите мыслить этим инженерно, держите в голове причинно‑следственную цепочку: “ожидаемый CSRF‑токен живёт в сессии; сессия истекла — ожидаемый токен исчез; запрос всё ещё пытается использовать старое значение; сервер ждёт другое — получаем invalid”.

С практической точки зрения это означает: при странном «вчера работало, сегодня не работает» вы первым делом проверяете не код, а состояние сессии. Иногда самое эффективное “исправление” в дев‑среде — просто перелогиниться и повторить запрос уже в свежем session context.

6. Отключение CSRF как анти‑паттерн

Есть особый вид “исправления ошибок”, который знаком каждому программисту: когда приложение ругается, мы не решаем проблему, а делаем так, чтобы оно перестало ругаться. Это как заклеить лампочку check engine изолентой: она правда больше не светится, но двигатель от этого не становится здоровее. В мире Spring Security такой изолентой часто становится csrf(csrf -> csrf.disable()). И да, это снимает боль. Но вместе с болью вы снимаете и защиту, а главное — вы теряете понимание, почему система ругалась.

Вот тот самый анти‑пример, который так и просится в конфиг после первого 403:

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // В browser/session сценарии это почти всегда "заклеить лампочку":
    // проблема исчезает, но вместе с ней исчезает и защита
    http.csrf(csrf -> csrf.disable()); // анти-паттерн для session/browser ветки

    // Не забывайте: конфиг должен собраться в цепочку фильтров
    return http.build();
}

Важно: мы не говорим, что CSRF никогда нельзя отключать. Мы говорим, что в текущей ветке курса, где мы осознанно изучаем browser/session‑модель, отключение CSRF — это как выкинуть учебник по математике, потому что дроби “бесят”. Возможно, станет легче жить, но знание не появится.

Есть ещё один практичный момент. Когда вы глобально отключаете CSRF, вы начинаете строить привычку: “если не работает — выключи”. Эта привычка очень опасна, потому что в реальных проектах вы часто не сможете “просто выключить” механизмы безопасности. Да и не должны.

Поэтому правильная последовательность действий такая: сначала диагностируем по алгоритму из предыдущего раздела, убеждаемся, что проблема действительно в токене/сессии, и только потом принимаем осознанные решения (и в учебном проекте, и в боевом). Сегодняшняя задача — научиться жить с CSRF‑проверкой и понимать её сигналы.

Логи на минималках: как быстро увидеть причину

Когда что-то падает “раньше контроллера”, логи становятся вашим лучшим другом. Не в смысле “включить всё на TRACE и утонуть”, а в смысле “подсветить именно security‑слой, чтобы увидеть, что происходит”. Это особенно полезно для CSRF, потому что вы можете не видеть никаких логов из приложения вообще — оно же не дошло до ваших log.info(...). Поэтому включение debug‑логов Spring Security на время обучения — отличный способ превратить “магический 403” в читаемое событие.

Минимальная настройка в application.yml может выглядеть так:

logging:
  level:
    # Включаем только security-слой: этого обычно достаточно для CSRF-диагностики
    org.springframework.security: DEBUG

Теперь, когда CSRF‑проверка не проходит, вы часто увидите в логах сообщения, которые прямо намекают на причину: что-то вроде “Invalid CSRF token …” или “Missing CSRF token …” (формулировки могут различаться, но смысл стабилен). С точки зрения обучения важнее даже не точный текст, а факт: проблема произошла внутри security filter chain, и вы можете это подтвердить не по ощущениям, а по наблюдаемым данным.

При этом важно держать здоровую дисциплину: такие логи — инструмент обучения и диагностики, а не “вечно включённый режим”. В продакшене вы обычно не хотите светить детали security‑отказов слишком подробно. Но мы сейчас не про продакшен‑политику, мы про то, чтобы вы научились отличать missing от invalid без паники.

7. Типичные ошибки при разборе CSRF‑сбоев

Когда человек впервые сталкивается с CSRF, он почти всегда совершает одни и те же ошибки — не потому что он “плохой”, а потому что мозг пытается объяснить новую механику через старые привычки (“если 403 — значит роли”). Сейчас мы аккуратно разберём эти ошибки так, чтобы они у вас больше не повторялись хотя бы каждый понедельник.

Ошибка №1: искать проблему в контроллере и сервисе, хотя запрос до них не дошёл.
Это самая дорогая по времени ошибка. Вы начинаете рефакторить метод, добавляете валидацию, меняете DTO, а потом внезапно понимаете, что ни одна строка бизнес‑кода вообще не выполнялась. Лечится она просто: если подозреваете CSRF, первым делом ставьте “маячок” (log.info(...)) в контроллер и проверяйте, доходит ли запрос. Если нет — вы в фильтрах, а не в MVC.

Ошибка №2: путать CSRF‑403 с “нет прав по роли”.
Проблема в том, что и “не хватает роли”, и “не прошёл CSRF” часто выглядят как 403. Но это разные ответы на разные вопросы. Роли и authorities — это “можно ли этому пользователю выполнять действие вообще”, а CSRF — “доверяем ли мы происхождению действия в browser/cookie‑сценарии”. Если вы сразу начинаете крутить hasRole, вы лечите не то место.

Ошибка №3: отправлять CSRF‑токен, но забывать про сессию (cookie).
Это типично, когда вы тестируете через curl или Postman. Вы нашли “какой-то токен”, добавили заголовок — и всё равно получаете отказ. Причина простая: ожидаемый токен связан с HttpSession. Нет правильного JSESSIONID — нет правильного “ожидаемого значения”. В итоге токен будет почти всегда invalid, даже если вы его честно нашли где-то в браузере.

Ошибка №4: не учитывать истечение сессии и «устаревание» токена.
В учебной разработке мы часто делаем паузы: прочитали лекцию, отошли, вернулись. Сессия могла истечь, особенно если у вас небольшой timeout. В результате первый же state‑changing запрос падает, и кажется, что “сломалось после перерыва”. На самом деле всё ожидаемо: сессия исчезла — ожидание токена изменилось — ваш старый токен больше не подходит. Если вы видите странную нестабильность, попробуйте сначала перелогиниться и повторить запрос.

Ошибка №5: первая реакция — отключить CSRF.
Это “короткая победа”: всё начинает работать, но вы выкинули из системы важный сигнал. В рамках session/browser ветки проекта такая реакция почти всегда означает, что вы ещё не диагностировали причину, а просто убрали проверку. Гораздо полезнее пройти четыре проверки из алгоритма: метод, cookie, наличие токена, соответствие токена сессии. Обычно на этом месте проблема становится очевидной и решаемой.

1
Задача
Spring Security, 11 уровень, 4 лекция
Недоступна
Missing token, invalid token и один успешный PATCH
Missing token, invalid token и один успешный PATCH
1
Задача
Spring Security, 11 уровень, 4 лекция
Недоступна
Устаревший token после session timeout
Устаревший token после session timeout
1
Опрос
CSRF Защита, 11 уровень, 4 лекция
Недоступен
CSRF Защита
Защита запросов от подделки
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ