JavaRush /Курсы /Spring Security /Этапы запроса в security c...

Этапы запроса в security chain

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

1. Этапы вместо списка фильтров

Список фильтров сам по себе почти не помогает. Когда впервые открываешь логи Spring Security или видишь перечень фильтров, легко попасть в ловушку: «Ого, тут 15–20 классов, сейчас выучу их названия — и всё пойму». Это примерно как пытаться стать поваром, выучив названия всех кастрюль на кухне. В этой лекции мы делаем более полезную вещь: переводим «толпу фильтров» в понятные этапы обработки запроса, где каждый этап отвечает на простой вопрос и готовит данные для следующего.

Важно понять цель: нам не нужен «каталог фильтров». Нам нужна схема, по которой вы сможете объяснить и себе, и коллеге: почему запрос дошёл до контроллера, почему не дошёл, почему появился redirect, почему «внезапно 403» и почему один и тот же endpoint ведёт себя по-разному в браузере и в curl.

Модель: запрос как конвейер «ворот»

Представьте аэропорт. Вы входите в здание — это ещё не значит, что вы уже в самолёте. Дальше идут этапы: рамка металлоискателя, проверка документов, досмотр ручной клади, и только потом — выход к гейтам. С безопасностью запросов история похожая: запрос не «телепортируется» в контроллер. Он проходит конвейер, где на каждом шаге либо получает нужный контекст, либо останавливается с понятным ответом.

Технически этот конвейер реализован внутри FilterChainProxy, который делегирует обработку конкретному SecurityFilterChain. Именно эта инфраструктура и создаёт ощущение «магии», если не видеть этапов. Но как только этапы складываются в голове, магия превращается в обычную инженерную механику: «на этапе X мы готовим контекст, на этапе Y пытаемся понять пользователя, на этапе Z принимаем решение по доступу».

Чтобы держать это в голове, будем использовать учебную схему из пяти этапов — это не «официальное название стадий», а удобный учебный разрез:

import java.util.List;

public class SecurityStagesDemo {

    public static void main(String[] args) {
        // Учебная модель: группируем фильтры не по классам, а по обязанностям (этапам).
        List<String> stages = List.of(
                // 1) Подготовить место, куда складываем security-состояние запроса
                "prepare SecurityContext",
                // 2) Попробовать аутентифицировать, если в запросе есть «документы»
                "try to authenticate (if request contains credentials)",
                // 3) Если пользователь так и не появился — сделать состояние явным через anonymous
                "fallback to anonymous (if still no user)",
                // 4) Превратить security-исключения в понятный HTTP-ответ (а не в stacktrace)
                "translate security exceptions into HTTP response",
                // 5) Принять решение по доступу до контроллера: пускать или нет
                "authorize request (allow / deny) before controller"
        );

        // Проверяем, что этапов действительно 5 (чисто для самопроверки схемы).
        System.out.println(stages.size()); // 5
    }
}

Эта схема специально упрощает реальность. Это учебная группировка обязанностей, а не буквальный инвентарь фильтров и не железный порядок, одинаковый для любого запроса. В реальной цепочке один фильтр может оборачивать следующие, часть этапов может выглядеть иначе, а набор authentication-фильтров зависит от того, как именно клиент предъявляет себя. Но по обязанностям картина всё равно собирается примерно так.

2. Этап 1 — подготовка SecurityContext: «не работаем в пустоте»

Любая серьёзная система начинается с того, что приводит «рабочее место» в порядок. В Spring Security этим «рабочим местом» является SecurityContext: контейнер, в котором живёт текущее security-состояние запроса. На первом этапе цепочка гарантирует, что контекст существует, пусть даже пустой, и что дальше фильтры могут не гадать «а где тут пользователь?», а смотреть в одно место.

На уровне инфраструктуры важно помнить, что FilterChainProxy — центральная точка servlet-безопасности Spring Security. В документации подчёркивается, что это отправная точка и что он делает обязательные вещи вроде очистки SecurityContext, чтобы не было утечек между запросами. Это как «после каждого пассажира протереть стол»: звучит скучно, но иначе очень быстро станет страшно.

В современных версиях Spring Security есть фильтр, который читает SecurityContext из хранилища и заполняет SecurityContextHolder, и дальше весь запрос работает через этот holder. Исторически это был SecurityContextPersistenceFilter, а позже документация описывает движение к SecurityContextHolderFilter, который читает и наполняет контекст, а сохранение делает более явным. Мы пока не углубляемся в то, где именно контекст хранится между запросами. Нам на сегодня важен сам факт: без контекста последующие этапы не могут принимать последовательные решения.

Мини-ощущение этого этапа можно получить даже без понимания деталей хранения:

import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

public class ContextPeek {

    public static void peek() {
        // SecurityContext обычно хранится в ThreadLocal через SecurityContextHolder:
        // это значит, что «текущий контекст» привязан к текущему потоку обработки запроса.
        SecurityContext context = SecurityContextHolder.getContext();

        // В реальном приложении тут обычно будет Authentication внутри контекста (или пусто).
        System.out.println(context); // (выведет объект контекста, формат зависит от реализации)
    }
}

Ключевая мысль: вы не создаёте SecurityContext руками в контроллере. Он подготавливается в security-слое, чтобы дальше любой код в рамках запроса мог спросить: «а кто сейчас делает запрос?» — и получить ответ из одного места.

3. Этап 2 — попытка понять пользователя: «данные для входа»

Когда контекст подготовлен, логично задать следующий вопрос: «а этот запрос от кого-то конкретного или просто прохожий?». На этом этапе фильтры смотрят на запрос и проверяют, есть ли в нём признаки того, что клиент пытается представиться. Это могут быть разные виды предъявления документов: заголовок Authorization, cookie, параметры формы логина и так далее. Нам пока важен сам момент появления входных данных, без разбора внутренней проверки логина и пароля.

Важный нюанс для новичка: фильтры на этом этапе не обязаны аутентифицировать пользователя. Они могут только понять, что данных нет, и тогда просто не трогать контекст или оставить его пустым до следующего этапа. А если данные есть, они запускают механизм аутентификации, который уже внутри решит: «верю / не верю». Нам сейчас важно увидеть само место в цепочке, где это вообще происходит.

Ниже для простоты — только примеры с заголовками. Это не значит, что Spring Security всегда ищет именно Authorization: credentials могут приезжать и через form login, и через cookie, и через другие механизмы.

Учебный псевдокод, который иллюстрирует идею, может выглядеть так:

public class CredentialsProbe {

    public static boolean hasCredentials(String authorizationHeader) {
        // Если заголовка нет — клиент не пытался предъявить «документы».
        if (authorizationHeader == null) {
            return false;
        }

        // Самые популярные схемы: Basic (логин/пароль) и Bearer (токен).
        // На этом этапе нас интересует только «есть ли попытка представиться».
        return authorizationHeader.startsWith("Basic ")
                || authorizationHeader.startsWith("Bearer ");
    }
}

Да, слово Bearer нам ещё рано любить, но как пример «в запросе есть попытка представиться» оно подходит. Смысл этапа прост: chain не угадывает пользователя телепатией, он либо находит входные данные, либо честно признаёт: «здесь нечего аутентифицировать».

4. Этап 3 — anonymous: «пусть это будет явное состояние, а не “ничего”»

После попытки аутентификации возможны два сценария: либо в контексте появляется реальный пользователь, либо по-прежнему никого. И вот здесь у Spring Security есть важная философия: «никого» — плохое состояние, потому что оно размазывает логику. Намного удобнее иметь явного anonymous principal, чтобы дальше все проверки работали единообразно.

Именно поэтому в цепочке присутствует AnonymousAuthenticationFilter. Название уже говорит само за себя: он превращает пустоту в технически описанное состояние. Список фильтров, который встречается в документации и JavaDoc, прямо включает AnonymousAuthenticationFilter среди типичных участников цепочки.

Почему это не лишняя бюрократия? Потому что дальше на этапе авторизации можно не писать разные ветки «если Authentication == null…». Вместо этого можно мыслить так: «есть authenticated user или anonymous user». И это аккуратно влияет на семантику вроде «пустить всех» или «пустить только вошедших».

Здесь нам важна сама роль этапа: пустота превращается в явную сущность; детали Authentication и Principal пока не нужны.

5. Этап 4 — перевод исключений в HTTP: ExceptionTranslationFilter

Если этапы 1–3 отвечают за «подготовить состояние запроса», то этап 4 — за «если что-то пошло не так, ответить клиенту нормально». Security-цепочка должна уметь остановить запрос предсказуемо: не упасть stacktrace-ом в ответ и не пустить исключение гулять до случайного обработчика. Для этого в chain есть фильтр, который переводит security-ошибки в HTTP-ответы.

В Spring Security это делает ExceptionTranslationFilter. В документации он прямо описан как механизм, который переводит AuthenticationException и AccessDeniedException в HTTP-ответы, используя AuthenticationEntryPoint для начала аутентификации и AccessDeniedHandler для обработки отказа в доступе.

Обратите внимание на важную механику: этот фильтр вызывает chain.doFilter(...), то есть пропускает запрос дальше, но если дальше кто-то бросит security-исключение, он его перехватит и превратит в ответ. В документации даже приводится псевдокод в духе try doFilter, catch exceptions, а дальше — либо запуск аутентификации, либо отказ в доступе.

Вот учебная мини-версия этой идеи:

public class ExceptionTranslationLike {

    public static String handle(boolean authenticated, boolean accessDenied) {
        try {
            // Симулируем ситуацию «доступ запрещён» где-то глубже по цепочке.
            // В реальности это были бы AccessDeniedException / AuthenticationException.
            if (accessDenied) {
                throw new RuntimeException("AccessDenied"); // упрощаем для примера
            }

            // Если ошибок нет — цепочка «пропускает запрос дальше».
            return "OK";
        } catch (RuntimeException ex) {
            // Важная развилка: если пользователь не аутентифицирован,
            // то ответ обычно будет про «начать аутентификацию».
            if (!authenticated) {
                return "Start authentication";
            }

            // Если пользователь аутентифицирован, но прав нет — «доступ запрещён».
            return "Access denied";
        }
    }
}

Это не Spring Security-код, а просто иллюстрация смысла: в одном месте цепочка решает, что сказать клиенту, когда доступ невозможен.

6. Этап 5 — авторизация запроса: «пустить дальше или нет»

Когда у нас уже есть понятное security-состояние — контекст и либо пользователь, либо anonymous, — цепочка подходит к главному практическому вопросу: «можно ли этому запросу идти дальше в DispatcherServlet, контроллеры и бизнес-логику?». Это и есть этап авторизации: проверка доступа.

В современных цепочках роль «проверить доступ» выполняют фильтры уровня авторизации, и в списке типичных фильтров вы встретите AuthorizationFilter, а также исторические элементы вроде FilterSecurityInterceptor. Нам сейчас не важно, как именно вы будете описывать правила, важно понять момент: решение по доступу может быть принято ещё до контроллера.

И вот здесь становится понятным наблюдение из прошлого дня: после подключения security-starter почти всё закрылось. Это произошло не потому, что контроллеры внезапно стали злыми и начали кидаться 403, а потому что на этапе авторизации security-цепочка сказала: «стоп, дальше нельзя» — и запрос закончился раньше MVC.

На уровне простой модели в голове это можно представить так:

public class AuthorizationDecisionDemo {

    public static String decide(String path, boolean authenticated) {
        // Условно считаем, что всё под /api/public/ — публичное.
        // (Как именно это настраивается в Spring Security — отдельная тема.)
        boolean isPublic = path.startsWith("/api/public/");

        // Публичное — пропускаем без условий.
        if (isPublic) return "ALLOW";

        // Не публичное — требуем, чтобы пользователь был «не anonymous».
        if (!authenticated) return "DENY (needs authentication)";

        // Если пользователь аутентифицирован — разрешаем.
        return "ALLOW";
    }
}

Мы не обсуждаем, как Spring Security узнаёт, что /api/public/** должно быть public, — это будет отдельная тема. Здесь важно закрепить другое: авторизация — отдельный этап, который использует результаты ранних этапов, то есть контекст и пользователь/anonymous.

7. Порядок этапов критичен: сломать всё шагом

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

Например, если авторизация начнётся раньше, чем цепочка решила «authenticated vs anonymous», то проверка будет либо постоянно видеть null, либо начнёт обрастать ветвлениями и превращаться в кашу. Поэтому идеи «давайте поменяем местами фильтры, чтобы работало» обычно заканчиваются тем, что оно начинает работать… но только в одном браузере и только по вторникам.

Учебная иллюстрация, почему anonymous перед access check логичнее, чем наоборот:

import java.util.List;

public class OrderingDemo {

    public static void main(String[] args) {
        // Неправильная логика: сначала проверяем доступ, потом «наводим порядок» с anonymous.
        List<String> wrongOrder = List.of("authorize", "set anonymous");

        // Правильная логика: сначала приводим состояние пользователя к явному виду,
        // и только потом принимаем решение по доступу.
        List<String> goodOrder = List.of("set anonymous", "authorize");

        // Печатаем просто чтобы визуально закрепить идею порядка.
        System.out.println(wrongOrder); // [authorize, set anonymous]
        System.out.println(goodOrder);  // [set anonymous, authorize]
    }
}

Если держать в голове именно зависимости этапов, реальный список фильтров перестаёт пугать. Он уже не выглядит случайным и начинает читаться как «много маленьких шагов, каждый из которых делает понятное дело».

8. Типичные ошибки при попытке понять этапы security chain

Ошибка №1: пытаться выучить Spring Security как список классов.
Такой подход часто приводит к эффекту «я всё выучил и всё равно ничего не понимаю». Названия фильтров похожи друг на друга, а реальная логика теряется. Намного полезнее закрепить пять этапов и только потом привязывать к ним конкретные классы вроде AnonymousAuthenticationFilter или ExceptionTranslationFilter.

Ошибка №2: думать, что security-решение принимается в контроллере.
После пары ситуаций вида «почему метод не вызвался?» новичок начинает подозревать, что Spring сломался. На самом деле запрос мог быть остановлен на этапе авторизации или на этапе обработки security-исключений — и до DispatcherServlet он просто не дошёл. Это абсолютно нормальный и правильный дизайн.

Ошибка №3: путать «нет пользователя» и «anonymous пользователь».
Когда кажется, что anonymous = null, начинаются странные проверки и хаос. В Spring Security anonymous — это намеренная модель состояния, чтобы последующие этапы работали единообразно.

Ошибка №4: не понимать роль ExceptionTranslationFilter и ждать, что любой сбой в security будет просто исключением.
Если не знать про этот этап, легко удивиться: «почему я не вижу stacktrace?» или «почему меня редиректит?». Перевод исключений в HTTP-ответы — обязанность security-слоя. Он перехватывает нужные исключения и превращает их в реакцию для клиента.

Ошибка №5: пытаться “пофиксить 403” перестановкой фильтров наугад.
Если менять порядок или набор фильтров без понимания зависимостей этапов, легко получить систему, которая вроде работает, но ломается на других типах запросов: например, в curl всё ок, а в браузере редиректы, или наоборот. Порядок этапов — это зависимость по данным, а не настройка вкуса.

1
Задача
Spring Security, 3 уровень, 2 лекция
Недоступна
Модель этапов через два фильтра и request attributes
Модель этапов через два фильтра и request attributes
1
Задача
Spring Security, 3 уровень, 2 лекция
Недоступна
Консольный симулятор этапов security chain
Консольный симулятор этапов security chain
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ