1. Два світи: container і Spring context
У вебзастосунку є два світи: servlet container і Spring ApplicationContext. Новачкові часто здається, що «все — Spring»: контролери — у Spring, сервіси — у Spring, конфігурація — у Spring, і навіть настрій, здається, теж. Але ці світи живуть поруч і не зобов’язані прямо знати один про одного. Перший світ — це servlet container (наприклад, вбудований Tomcat), який керує життєвим циклом HTTP-запитів, фільтрів і сервлетів. Другий — Spring ApplicationContext, де живуть ваші біни, конфігурація і вся «магія» DI.
У світі сервлетів правила прості: контейнер знає про інтерфейс jakarta.servlet.Filter, уміє створювати такі об’єкти, викликати doFilter() на кожен запит і дотримуватися порядку фільтрів. Контейнеру не дуже цікаво, що у вас там за @Service і @Bean: він, грубо кажучи, живе за своїми законами і не зобов’язаний читати ваші анотації.
У світі Spring усе навпаки: ApplicationContext знає, як створювати й зв’язувати біни, як уводити залежності, як застосовувати конфігурацію і як підключати автоконфігурацію. Але ApplicationContext сам по собі не є HTTP-сервером і не стоїть «на вході» кожного запиту без інтеграції з контейнером.
Звідси й виникає питання: як контейнер викликає Spring Security, якщо Spring Security реалізовано як Spring-біни? І ось тут нам потрібен міст — об’єкт, який стоїть у servlet chain, але вміє делегувати роботу Spring’у. Цим мостом і є DelegatingFilterProxy.
Щоб зафіксувати контекст, корисно пам’ятати: стек у нас servlet-based (Spring MVC), тобто фільтри і сервлети — це базовий «конвеєр» запитів. Зараз ми не розглядаємо 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), але всередині він працює не як «одна перевірка». Він зберігає список об’єктів SecurityFilterChain, і для кожного є правило: до яких запитів він застосовується. Це важливий перехід від моделі «один фільтр» до моделі «маршрутизація за типами запитів».
Ще одна ключова думка: 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-ланцюжків = " + proxy.getFilterChains().size());
};
}
Якщо у вас підключено spring-boot-starter-security і ви ще не писали власну конфігурацію, ви, швидше за все, побачите 1 (один ланцюжок, який «ловить усе підряд»). Пізніше, коли ви почнете свідомо розводити зони застосунку, картина може змінитися — але це вже історія для інших днів курсу.
Практична користь розуміння FilterChainProxy дуже проста: коли ви бачите в логах або стек-трейсі згадку 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 | Ланцюжок фільтрів сервлетного контейнера | Міст: передає запит із контейнера в Spring-бін | Так, servlet Filter |
| FilterChainProxy | Spring ApplicationContext | Диспетчер: обирає відповідний SecurityFilterChain і запускає його | Так, servlet Filter (Spring Security) |
| SecurityFilterChain | Всередині Spring Security | «Сценарій»: matcher + список security-фільтрів для запиту | Ні, це ланцюжок/набір (але всередині є фільтри) |
Якщо ви запам’ятаєте цю таблицю, то половина майбутньої «Spring Security магії» перетвориться просто на інженерну схему.
5. Схема делегування
Зараз саме час звести всю схему делегування в одну картинку. Важливо не лише знати визначення, а й бачити маршрут: хто стоїть на вході, хто кого викликає і де саме закінчуються повноваження кожного учасника. Без цього Spring Security виглядає як туман: усе ніби відбувається «десь до контролера», але незрозуміло де саме.
Ось спрощена схема (без деталей про конкретні security-фільтри — це вже наступна лекція):
flowchart TD
%% Спрощений маршрут запиту: контейнер → міст → Spring Security → MVC
Client["HTTP-клієнт"] --> Container["Сервлетний контейнер"]
Container --> Filters["Загальні servlet-фільтри"]
Filters --> DFP["DelegatingFilterProxy"]
DFP --> FCP["Бін FilterChainProxy: springSecurityFilterChain"]
FCP --> SFC["SecurityFilterChain: matches + filters"]
SFC --> MVC["DispatcherServlet → контролери"]
MVC --> Client
Ця діаграма відповідає на дуже практичне питання: «чому я не бачу нічого в логах контролера, але запит уже отримав 401/403/redirect?» Тому що до контролера ваш запит взагалі може не дійти. Він зупинився всередині FilterChainProxy або одного з його security-ланцюжків, а servlet container чесно повернув відповідь клієнту.
Якщо розкласти це по кроках, вийде так. Клієнт стукає в застосунок, контейнер приймає запит і проганяє його через ланцюжок своїх servlet-фільтрів. Серед них є 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. Отже, відповідь «запит не дійшов до контролера» — це не тупик, а добра діагностична точка: ви точно знаєте, де шукати причину і які сутності беруть участь.
А ще це пояснює, чому різні клієнти отримують різні реакції. Браузер частіше побачить редирект на сторінку входу, а 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: намагатися надто рано “полагодити” поведінку, не розуміючи, хто саме її створює.
Коли застосунок починає редиректити на сторінку входу або віддавати 401, дуже хочеться одразу «вимкнути» щось одним рядком. Але якщо ви поки не розрізняєте DelegatingFilterProxy і FilterChainProxy, ви не розумієте, що саме вимикаєте і де знаходиться причина. Наша мета в модулі 1 — спочатку зібрати внутрішню карту, а вже потім писати власну конфігурацію (це буде на Дні 5).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ