1. Модель: endpoint ≠ контролер
Наявність URL ще не означає, що запит дійшов до методу контролера. Майже кожен новачок — і, якщо чесно, багато досвідчених людей — спершу думає так: є URL, отже є метод контролера, отже запит «потрапив у метод» і там «щось сталося». Це зручно й зрозуміло, але на реальному сервері між «прийшов HTTP-запит» і «виконався метод контролера» є цілий коридор інфраструктури. І саме в цьому коридорі розташована безпека.
Уявіть, що контролер — це кабінет лікаря. Пацієнт (HTTP-запит) не телепортується одразу до лікаря. Спочатку він заходить у будівлю (у застосунок), проходить реєстратуру, охорону, можливо, гардероб (фільтри) — і тільки потім потрапляє до кабінету (контролера). Якщо охорона вирішила, що пройти не можна (немає перепустки, не той час, не та роль), кабінет лікаря навіть не дізнається, що хтось приходив. Це не «помилка кабінету». Це правильно організований вхід у будівлю.
Саме тому на курсі з Spring Security ми так наполегливо повторюємо: «не шукайте проблему лише в контролері». Дуже часто контролер просто не викликається, тому що рішення ухвалене раніше.
Servlet container: перший в обробці
Слова servlet container звучать як щось із роману Лавкрафта, але насправді це цілком земна річ. Servlet container — це середовище, яке приймає вхідні HTTP-запити й запускає їх обробку за правилами Servlet API. У Spring Boot-застосунку це зазвичай вбудований сервер (наприклад, Tomcat), який стартує разом із застосунком і починає слухати порт.
Коли ви запускаєте Spring Boot, відбувається важлива «магія» (насправді — інженерія): підіймається контейнер, реєструються сервлети й фільтри, створюється Spring ApplicationContext, і контейнер вчиться викликати частину Spring-застосунку. Але головне запам’ятати ось що: першим запит бачить контейнер, а не Spring MVC і не ваш контролер.
Якщо спростити, обробка запиту виглядає так:
1) запит прийшов у контейнер;
2) контейнер прогнав запит через ланцюг servlet filters;
3) контейнер передав запит одному з сервлетів (у нашому випадку — у DispatcherServlet Spring MVC);
4) Spring MVC знайшов контролер і викликав метод;
5) відповідь пішла назад тим самим шляхом, тільки у зворотному напрямку.
І саме в пункті №2 — у ланцюгу фільтрів — розташована значна частина Spring Security. Тому питання «де знаходиться Spring Security?» — не філософське, а цілком практичне: у якій точці обробки запиту він стоїть.
2. Servlet filters: рамка навколо запиту
Фільтр у Servlet API — це елемент конвеєра, який може виконати код до того, як запит потрапить у servlet, і після того, як servlet завершить роботу. Цей ефект «обгортки» дуже важливий: фільтр може не тільки логувати, а й змінювати запит і відповідь, а іноді — взагалі завершити обробку раніше, ніж хтось дістанеться до MVC.
Фільтр зручно уявляти як «рамку», яка обгортає все, що йде далі:
- «до» — підготовка, перевірки, логування;
- chain.doFilter(...) — передавання далі;
- «після» — завершення, метрики, фінальні заголовки, аудит.
Мінімальний навчальний фільтр (у нашому проєкті його можна тимчасово розмістити, наприклад, у com.example.securecontent.common.web) матиме такий вигляд:
package com.example.securecontent.common.web;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // Виконуємо фільтр якомога раніше, щоб побачити його у виведенні
public class TraceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// "До" — виконується перед передаванням запиту далі по ланцюгу (включно зі Spring Security / MVC)
System.out.println("Фільтр: до контролера");
// Ключовий виклик: передаємо керування наступному фільтру/сервлету
chain.doFilter(request, response);
// "Після" — виконується, коли обробка запиту нижче по ланцюгу завершилася
System.out.println("Фільтр: після контролера");
}
}
Дві важливі деталі. По-перше, chain.doFilter(...) — це буквально «передай керування наступному». По-друге, я додав @Order(Ordered.HIGHEST_PRECEDENCE), щоб фільтр виконувався якомога раніше. Нам це потрібно не для бойового середовища, а щоб у навчальному експерименті ви гарантовано побачили його виведення, навіть якщо інші фільтри захочуть зупинити запит.
А тепер най«чесніша» частина. Фільтр може взагалі не пустити запит далі. Це звучить суворо, але саме так і має працювати безпека: якщо не можна — значить не можна, і немає сенсу турбувати контролер. Ось максимально наочний приклад «перекрили вхід до кабінету»:
package com.example.securecontent.common.web;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class BlockMeFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// У Servlet API приходять базові інтерфейси — приводимо їх до HTTP-специфічних типів
HttpServletRequest req = (HttpServletRequest) request;
// Навчальна логіка: забороняємо доступ до конкретного URI "на вході", ще до MVC/контролера
if ("/api/me".equals(req.getRequestURI())) {
HttpServletResponse resp = (HttpServletResponse) response;
resp.setStatus(403); // Повертаємо 403 Forbidden і не пропускаємо запит далі по ланцюгу
return; // Важливо: без return ланцюг продовжиться
}
// Якщо не блокуємо — продовжуємо звичайну обробку
chain.doFilter(request, response);
}
}
Цей фільтр — навчальна «пародія» на ідею security. У реальному житті ви не будете так робити для авторизації (для цього в нас Spring Security), але як демонстрація того, що контролер може взагалі не викликатися, він працює ідеально.
3. DispatcherServlet: вхід у Spring MVC
DispatcherServlet — це центральний servlet Spring MVC, який реалізує патерн «Front Controller». Тобто контейнер передає запит не напряму в контролер, а в DispatcherServlet, а той уже всередині Spring MVC вирішує, який контролер підходить цьому запиту, як зв’язати path variables, як розпарсити тіло, як застосувати конвертери, як викликати метод і як сформувати відповідь.
Якщо зовсім спростити до рівня «щоб не вибухнув мозок», DispatcherServlet працює за схемою: «отримав запит — знайшов обробник — викликав обробник — перетворив результат на відповідь». І для нашої лекції тут найважливіше одне: DispatcherServlet живе після фільтрів. Усе, що ми обговорювали в попередньому розділі, може статися ще до MVC.
Щоб відчути це руками, достатньо мати простий контролер. Наприклад, нашу кінцеву точку особистої зони:
package com.example.securecontent.profile;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MeController {
@GetMapping("/api/me") // Кінцева точка "особистої зони": на ній зручно спостерігати, чи блокує нас security
public String me() {
// Маркер: якщо цей рядок не друкується, значить запит не дійшов до контролера
System.out.println("Викликано метод контролера");
return "me"; // Навчальна відповідь
}
}
Якщо ви робите запит і цей рядок не виводиться, це не означає, що у вас «зламався контролер». Це означає, що запит до нього не дійшов. Причини бувають різні, але на курсі з безпеки ми майже завжди спочатку підозрюємо фільтри та ланцюг безпеки.
Від цього моменту корисно виробити звичку: коли бачите дивну поведінку кінцевої точки, спочатку запитайте себе: «А запит взагалі дійшов до DispatcherServlet?» Якщо ні — дивимося фільтри та security.
4. Spring Security у ланцюгу фільтрів
Тепер зберемо все в одну картину. Spring Security у servlet-застосунку — це не один «чарівний if», а інфраструктура на рівні фільтрів. Запит спочатку проходить через servlet filter chain, і десь усередині неї стоїть security-частина, яка може: (а) пропустити запит далі, (б) доповнити обробку даними, щоб застосунок розумів, у якому стані перебуває запит, або (в) зупинити запит і повернути відповідь клієнту.
Найважливіше в цій лекції — запам’ятати не назви класів (їх буде багато), а місце в життєвому циклі запиту. Якщо security-рішення ухвалене до DispatcherServlet, то ваш контролер може не виконатися, ваші сервіси не викличуться, а репозиторій не побачить запиту. Для вас це іноді виглядає як «я ж написав метод, чому він не працює?», але насправді «він і не мав запускатися».
Давайте намалюємо максимально просту схему. Вона навчальна, але дуже «практична» — щоб у вас у голові перестала жити модель «URL → метод».
flowchart TD
Client["Клієнт: браузер / Postman / curl"]
Container["Servlet container"]
Filters["Servlet filters"]
Security["Spring Security (як частина ланцюга фільтрів)"]
DS["DispatcherServlet (Spring MVC)"]
Controller["Метод контролера"]
Response["HTTP-відповідь"]
Client --> Container --> Filters --> Security --> DS --> Controller --> Response
Тепер прив’яжемо це до нашого проєкту Secure Content Platform API. У нас є, умовно, дві зони:
- /api/public/articles — публічна зона (за бізнес-сенсом);
- /api/me — особиста зона, куди анонімним користувачам точно не можна.
На рівні коду контролера вони можуть виглядати однаково просто — два методи. Але в життєвому циклі запиту різниця вже суттєва: security зупинить /api/me раніше, а /api/public/articles пропустить. Ми вже спостерігали цей ефект на практиці: starter-security «закрив усе», і ви отримали інші відповіді навіть без своєї конфігурації.
І ось головний висновок: Spring Security — це механізм, який стоїть «на вході», раніше за MVC, тому він здатен змінити долю запиту ще до того, як ваш код узагалі його побачить.
Але сказати лише «security живе у фільтрах» мало. Якщо першим запит бачить контейнер, далі виникає природне питання: як цей servlet-світ узагалі передає керування Spring-бінам безпеки?
5. Мініексперимент: шлях запиту
Зараз буде невеликий експеримент, щоб ви не просто прочитали теорію, а побачили причинно-наслідковий зв’язок. Ми не писатимемо в цій лекції конфігурацію безпеки — це буде пізніше, але можемо самі «підсвітити» ланцюг: фільтр пише повідомлення, контролер пише повідомлення, а ви порівнюєте, що сталося насправді.
Якщо ви додали TraceFilter і MeController, то у вас з’являються три можливі сценарії:
Сценарій A: запит не дійшов до контролера через security (наприклад, ви не аутентифікувалися). Тоді ви, найімовірніше, побачите лише виведення фільтра «до», а «Викликано метод контролера» — ні. Іноді ви побачите ще й «після» (якщо ланцюг повернув керування назад), але самого контролера не буде.
Сценарій B: запит дійшов до контролера (наприклад, ви зробили запит із правильними обліковими даними для користувача за замовчуванням із другого дня). Тоді ви побачите:
- «Фільтр: до контролера»
- «Викликано метод контролера»
- «Фільтр: після контролера»
Це і буде доказом, що контролер справді знаходиться посередині ланцюга, а не стоїть «першим».
Сценарій C: запит не дійшов до контролера, тому що його зупинив «свій» фільтр (наприклад, BlockMeFilter). Тоді ви теж побачите, що контролер мовчить, але причина буде не у Spring Security, а у вашому навчальному блокаторі. Це корисно: ви починаєте відрізняти «не дійшло до MVC» від «дійшло, але впало в контролері».
Щоб було простіше звіряти очікування, ось типова логіка виведення (псевдоприклад; не до байта, але за змістом саме так):
Фільтр: до контролера
Фільтр: після контролера
Якщо контролер не викликався, рядка «Викликано метод контролера» немає. І це вже цінна інформація.
6. Типові помилки під час роботи з ланцюгом фільтрів
Помилка №1: шукати причину відмови лише в контролері.
Коли ви бачите 401, 403, редирект на вхід або «у Postman працює інакше», дуже легко почати виправляти @GetMapping, DTO і сервіси. Але якщо запит не дійшов до DispatcherServlet, усі ці правки — як лагодити дверну ручку в кімнаті, куди вас не пустили охоронці.
Помилка №2: думати, що фільтр — це лише «логування для дорослих».
На практиці фільтр — це точка, яка може повністю змінити долю запиту. Він може зупинити обробку, виставити статус, додати заголовки, зробити редирект, не пустити запит у MVC. І Spring Security саме так і працює: він не «радить контролеру», а діє раніше.
Помилка №3: плутати DispatcherServlet і контролер.
Контролер — це ваш код із @RestController. DispatcherServlet — інфраструктурний вхід у MVC. Між ними багато механіки. Якщо ви їх не розрізняєте, то неминуче почнете думати «Spring MVC = контролер», а це знову повертає до хибної моделі «URL → метод».
Помилка №4: намагатися пояснити всю безпеку анотаціями на кінцевих точках.
Анотації на контролері важливі, але вони не відповідають на запитання «коли взагалі цей метод викличеться». Якщо security зупинив запит у фільтрах, жодна анотація «всередині MVC» не допоможе — тому що MVC до неї просто не дійшов.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ