JavaRush /Курсы /Spring Security /DelegatingFilterProxy

DelegatingFilterProxy и цепочки Security

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

1. Два мира: container и Spring context

У web-приложения есть два мира: servlet container и Spring ApplicationContext. Новичку часто кажется, что «всё — Spring»: контроллеры Spring, сервисы Spring, конфиг Spring, и даже настроение Spring. Но эти миры живут рядом и не обязаны напрямую знать друг о друге. Первый мир — это servlet container (например, встроенный Tomcat), который управляет жизненным циклом HTTP-запросов, фильтров и servlet’ов. Второй — Spring ApplicationContext, где живут ваши бины, конфигурация и вся «магия» DI.

В мире servlet правила простые: контейнер знает про интерфейс jakarta.servlet.Filter, умеет создавать такие объекты, вызывать doFilter() на каждый запрос и соблюдать порядок фильтров. Контейнеру не особенно интересно, что у вас там за @Service и @Bean, — он, грубо говоря, живёт по своим законам и не обязан читать ваши аннотации.

В мире Spring всё наоборот: ApplicationContext знает, как создать и связать бины, как внедрить зависимости, как применить конфигурацию, как подключить автоконфигурацию. Но ApplicationContext сам по себе не является HTTP-сервером и не стоит «на входе» каждого запроса без интеграции с контейнером.

Отсюда и появляется вопрос: как контейнер вызывает Spring Security, если Spring Security реализован как Spring-бины? И вот здесь нам нужен мост — объект, который стоит в servlet chain, но умеет делегировать работу Spring’у. Этим мостом и является DelegatingFilterProxy.

Чтобы зафиксировать контекст, полезно помнить, что стек у нас servlet-based (Spring MVC), то есть фильтры и servlet’ы — это базовый «конвейер» запросов. Мы сейчас не обсуждаем WebFlux и reactive-модель: там другая механика, и сегодня она только мешала бы.

2. DelegatingFilterProxy: фильтр-мост

Слово Proxy у новичков часто вызывает две реакции. Первая: «это что-то про VPN». Вторая: «это что-то про AOP». На самом деле здесь всё гораздо приземлённее: DelegatingFilterProxy — это обычный servlet filter, который живёт в мире контейнера, но не содержит бизнес-логики безопасности. Его задача — быть «переходником»: взять входящий запрос и передать его фильтру, которым управляет Spring и который лежит в ApplicationContext.

Ключевой принцип DelegatingFilterProxy прост: контейнер вызывает его как фильтр, а он находит в ApplicationContext другой фильтр и делегирует ему работу. В контексте Spring Security этот «другой фильтр» обычно — FilterChainProxy, и часто он зарегистрирован под стандартным именем бина springSecurityFilterChain. Поэтому, когда вы видите в тексте «springSecurityFilterChain», не думайте, что это какая-то «магическая строка из ритуала». Это вполне конкретный бин-фильтр, которому делегируют вход запроса.

Очень важно понять роль: DelegatingFilterProxy не решает, «пускать или не пускать», не проверяет логин/пароль и не хранит пользователей. В этой истории он как охранник на проходной, который сам не проверяет ваши права, а просто звонит в службу безопасности: «К вам человек пришёл, разберитесь». Иногда новичку кажется, что «если это Security filter, значит он и решает». Нет: он делегирует.

Упрощённая (учебная) иллюстрация идеи делегирования может выглядеть так:

import jakarta.servlet.*;
import java.io.IOException;

import org.springframework.context.ApplicationContext;

public class DelegatingFilterProxyLike implements Filter {

    // Spring-контекст: в реальности его получение/поиск делается аккуратнее,
    // но учебно считаем, что он у нас уже есть.
    private final ApplicationContext context;

    public DelegatingFilterProxyLike(ApplicationContext context) {
        // Сохраняем ссылку на контекст, чтобы потом доставать бин-фильтр.
        this.context = context;
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        // Достаём "настоящий" security-фильтр как Spring-бин по имени.
        Filter delegate = context.getBean("springSecurityFilterChain", Filter.class);

        // Ключевой момент: решения принимает делегат, а не этот proxy-фильтр.
        // Поэтому chain "продвигается" уже изнутри delegate.
        delegate.doFilter(req, res, chain);
    }
}

Обратите внимание на психологически важную деталь: chain.doFilter(...) здесь вызывает не DelegatingFilterProxyLike, а уже делегат. В этом и смысл: контейнер думает, что у него «один фильтр», а внутри этого фильтра спрятан целый мир.

В реальности DelegatingFilterProxy ещё аккуратно решает вопрос доступа к ApplicationContext (потому что контейнер и Spring поднимаются не в вакууме), но нам сейчас важно не тонкое устройство поиска контекста, а сам факт: это мост.

3. FilterChainProxy: диспетчер цепочек

После того как DelegatingFilterProxy передал управление в Spring-мир, начинается то, что обычно и называют «Spring Security». Центральный объект, который стоит на входе Security-части, — это FilterChainProxy. Его удобно воспринимать как диспетчера: он принимает запрос и решает, какая именно security-логика должна к нему примениться.

FilterChainProxy сам является Filter (то есть его можно вызвать так же, как обычный servlet filter), но внутри он работает не как «одна проверка». Он хранит список security-цепочек, и для каждой есть правило: к каким запросам она относится. Это важный переход от модели «один фильтр» к модели «маршрутизация по типам запросов».

Ещё одна ключевая мысль: FilterChainProxy — это уже Spring bean. Значит, его можно внедрять, исследовать, логировать — в общем, он живёт по правилам ApplicationContext. Для учебного проекта это особенно приятно: мы можем «пощупать» его как любой другой компонент.

Например, можно в нашем приложении (временно, для обучения) вывести, сколько security-цепочек собрано в приложении. Это не настройка доступа и не «боевой код», а инструмент, чтобы глазами увидеть реальность:

import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.security.web.FilterChainProxy;

@Bean
ApplicationRunner printSecurityChains(FilterChainProxy proxy) {
    return args -> {
        // Учебная диагностика: сколько цепочек собрано Spring Security.
        // Это помогает понять, "одна цепочка на всё" у нас сейчас или несколько.
        System.out.println("Security chains = " + proxy.getFilterChains().size());
    };
}

Если у вас подключён spring-boot-starter-security и вы ещё не писали собственную конфигурацию, вы, скорее всего, увидите 1 (одну цепочку, которая «ловит всё подряд»). Позже, когда вы начнёте осознанно разводить зоны приложения, картина может измениться — но это уже история для других дней курса.

Практическая польза понимания FilterChainProxy очень простая: когда вы видите в логах или stacktrace упоминание FilterChainProxy, вы можете честно сказать себе: «Окей, я точно внутри Spring Security. Это не контроллер, не DispatcherServlet и не Jackson». А это уже здорово экономит время.

4. SecurityFilterChain: matcher и фильтры

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

В учебном приближении интерфейс выглядит примерно так:

import jakarta.servlet.Filter;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;

public interface SecurityFilterChain {

    // Отвечает на вопрос: "Этот запрос обслуживается этой security-цепочкой?"
    boolean matches(HttpServletRequest request);

    // Возвращает список фильтров, которые будут применены к запросу,
    // если matches(...) вернул true.
    List<Filter> getFilters();
}

Эта сущность методически очень важна: она помогает перестать думать, что «Spring Security = один фильтр». На самом деле это «набор фильтров, собранных в цепочку», и цепочка выбирается по условию.

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

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

  • FilterChainProxy хранит несколько SecurityFilterChain.
  • Каждая SecurityFilterChain отвечает за свой тип запросов.
  • Выбор цепочки делается через matcher (matches(...)), а затем выполняется набор filters (getFilters()).
  • На ранних этапах курса (и часто в приложении «по умолчанию») есть одна цепочка, которая применяется ко всем запросам.
  • Позже иногда может появиться несколько цепочек, но это уже отдельная тема, и мы не будем забегать вперёд.

Очень полезно один раз сравнить три сущности в одной таблице — мозг любит таблички, потому что они уменьшают хаос:

Сущность Где живёт Что делает простыми словами Это «фильтр»?
DelegatingFilterProxy Servlet container filter chain Мост: передаёт запрос из контейнера в Spring-бин Да, servlet Filter
FilterChainProxy Spring ApplicationContext Диспетчер: выбирает подходящую SecurityFilterChain и запускает её Да, servlet Filter (Spring Security)
SecurityFilterChain Внутри Spring Security «Сценарий»: matcher + список security-фильтров для запроса Нет, это цепочка/набор (но внутри есть фильтры)

Если вы запомните эту таблицу, то половина будущей «Spring Security магии» превратится просто в инженерную схему.

5. Схема делегирования

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

Вот упрощённая схема (без деталей про конкретные security-фильтры — это уже следующая лекция):

flowchart TD
    %% Упрощённый маршрут запроса: контейнер → bridge → Spring Security → MVC
    Client["HTTP client"] --> Container["Servlet container"]
    Container --> Filters["Servlet filters общие"]
    Filters --> DFP["DelegatingFilterProxy"]
    DFP --> FCP["FilterChainProxy bean: springSecurityFilterChain"]
    FCP --> SFC["SecurityFilterChain matches + filters"]
    SFC --> MVC["DispatcherServlet → Controllers"]
    MVC --> Client

Эта диаграмма отвечает на очень практический вопрос: «почему я не вижу ничего в логах контроллера, но запрос уже получил 401/403/редирект?» Потому что до контроллера ваш запрос вообще может не дойти. Он остановился внутри FilterChainProxy и его SecurityFilterChain, а servlet container честно вернул ответ клиенту.

Если разложить это по шагам, получится так. Клиент стучится в приложение, контейнер принимает запрос и прогоняет его через цепочку своих servlet filters. Среди них есть DelegatingFilterProxy, который не принимает решения, а просто делегирует обработку в Spring-бин. Этот бин — FilterChainProxy. Он выбирает подходящую SecurityFilterChain (или одну-единственную цепочку, если она подходит под все запросы) и запускает внутренние security-фильтры. И только если цепочка решила «да, продолжаем», запрос попадёт дальше — в DispatcherServlet, а потом в контроллер.

Это та самая штука, которая превращает фразу «Spring Security работает через фильтры» из мантры в нормальное понимание.

6. Пример: Secure Content Platform API

В учебном проекте легко отмахнуться: «Ну это просто теория про какие-то proxy, у меня же контроллеры важнее». Но если вы не понимаете, где находится DelegatingFilterProxy и кто такой FilterChainProxy, вы будете отлаживать security, глядя в контроллер, — и чувствовать себя человеком, который пытается чинить лифт, стоя в квартире и ругаясь на кнопку вызова.

Давайте привяжем всё к домену проекта. У нас есть публичная зона, например:

GET /api/public/articles

И личная зона:

GET /api/me

Типичные «ожидания по продукту» такие: статьи должны читаться анонимно, а /api/me должен требовать аутентификацию. Но после подключения spring-boot-starter-security мы уже видели, что в конфигурации по умолчанию закрывается почти всё — и /api/public/articles, и /api/me могут вести себя одинаково строго.

Чтобы проверить это на проекте, второй раз писать тот же демонстрационный код контроллера не нужно. Достаточно оставить уже знакомый маркерный лог в /api/me и /api/public/articles: если после запроса его нет, значит запрос остановили раньше MVC. Это как раз тот случай, где «контроллер молчит» — полезная диагностика, а не тупик.

Теперь внимание: если после подключения security вы делаете запрос к /api/me и не видите маркер из контроллера, это не «почему-то не работает контроллер». Это означает, что запрос остановили раньше. И если вы делаете запрос к /api/public/articles и тоже не видите его, это тоже не удивительно: в конфигурации по умолчанию текущая SecurityFilterChain может применяться к любому запросу и требовать аутентификацию.

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

А ещё это объясняет, почему разные клиенты получают разные реакции. Браузер чаще увидит редирект на login page, а curl получит 401 Unauthorized с заголовком challenge. Эти различия рождаются внутри security-цепочки, а не в MVC-слое. Пока нам важно просто увидеть место, где это физически происходит: внутри выбранной SecurityFilterChain, которую запускает FilterChainProxy.

7. Типичные ошибки при работе с цепочками Security

Когда студент впервые встречает DelegatingFilterProxy, FilterChainProxy и SecurityFilterChain, мозг обычно пытается сделать простую оптимизацию: «Ладно, это всё какой-то фильтр, неважно». Реакция понятная — мозг вообще любит экономить энергию, — но именно здесь такая экономия дорого стоит.

Ошибка №1: считать все три сущности синонимами слов “security filter”.
В результате вы перестаёте видеть архитектуру: DelegatingFilterProxy — мост в servlet-цепочке контейнера, FilterChainProxy — Spring Security-диспетчер внутри ApplicationContext, а SecurityFilterChain — выбранный сценарий (цепочка фильтров) для запроса. Если всё назвать одним словом, вы теряете ответы на вопросы «где это живёт?» и «кто кого вызывает?».

Ошибка №2: искать “правила доступа” в DelegatingFilterProxy.
DelegatingFilterProxy почти никогда не является местом, где «принимают решения». Он делегирует. Если вы пытаетесь найти внутри него «где там проверяется роль» или «где там решается 401/403», вы тратите время не там. Решения происходят уже глубже — в FilterChainProxy и фильтрах внутри SecurityFilterChain.

Ошибка №3: думать, что SecurityFilterChain — это “цепочка servlet container’а”.
Есть «внешняя» цепочка фильтров контейнера (туда входят ваши реализации Filter и тот же DelegatingFilterProxy) и «внутренняя» security-цепочка Spring Security (SecurityFilterChain). Это две разные цепочки, одна вложена в другую. Путаница здесь обычно приводит к странным выводам вроде «почему у меня фильтры выполняются дважды» или «почему мой фильтр не влияет на security».

Ошибка №4: отлаживать security по контроллеру, не проверив, дошёл ли запрос до MVC.
Пока у вас нет этой лестницы делегирования в голове, очень легко попасть в ловушку: «у меня контроллер не работает» → вы правите @RestController → ничего не меняется → вы становитесь грустным. В нормальной модели вы сначала задаёте вопрос: «Запрос вообще дошёл до DispatcherServlet?» Если нет — значит, проблема в фильтрах, и для security это совершенно нормальная ситуация.

Ошибка №5: пытаться слишком рано “чинить” поведение, не понимая, кто именно его создаёт.
Когда приложение начинает редиректить на login page или отдавать 401, очень хочется сразу «отключить» что-то одной строкой. Но если вы пока не различаете DelegatingFilterProxy и FilterChainProxy, вы не понимаете, что именно отключаете и где находится причина. Наша цель в модуле 1 — сначала собрать внутреннюю карту, а уже потом писать собственную конфигурацию (это будет на Дне 5).

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