1. Модель endpoint = контроллер
URL ещё не означает, что запрос дошёл до метода контроллера. Почти каждый новичок — и, если честно, многие опытные люди — поначалу думает так: есть URL, значит есть метод контроллера, значит запрос «попал в метод» и там «что-то произошло». Это удобно и понятно, но на реальном сервере между «пришёл HTTP-запрос» и «выполнился метод контроллера» есть целый коридор инфраструктуры. И именно в этом коридоре живёт безопасность.
Представьте, что контроллер — это кабинет врача. Пациент (HTTP-запрос) не телепортируется сразу к врачу. Сначала он заходит в здание (в приложение), проходит регистратуру, охрану, возможно, гардероб (фильтры) — и только потом попадает в кабинет (контроллер). Если охрана решила, что пройти нельзя (нет пропуска, не то время, не та роль), кабинет врача даже не узнает, что кто-то приходил. Это не «ошибка кабинета». Это правильно устроенный вход в здание.
Именно поэтому в курсе по Spring Security мы так настойчиво повторяем: «не ищите проблему только в контроллере». Очень часто контроллер просто не вызывается, потому что решение принято раньше.
Servlet container: первый в обработке
Слова servlet container звучат как что-то из романа Лавкрафта, но на деле это вполне земная штука. Servlet container — это среда, которая принимает входящие HTTP-запросы и запускает их обработку по правилам Servlet API. В Spring Boot приложении это обычно встроенный сервер (например, Tomcat), который стартует вместе с приложением и начинает слушать порт.
Когда вы запускаете Spring Boot, происходит важная «магия» (на самом деле — инженерия): поднимается контейнер, регистрируются servlets и filters, создаётся Spring ApplicationContext, и контейнер учится вызывать Spring-часть приложения. Но главное запомнить вот что: первым запрос видит контейнер, а не Spring MVC и не ваш контроллер.
Если упростить, обработка запроса выглядит так:
1) запрос пришёл в контейнер;
2) контейнер прогнал запрос через цепочку servlet filters;
3) контейнер передал запрос одному из servlets (в нашем случае — в 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("Filter: before controller");
// Ключевой вызов: передаём управление следующему фильтру/servlet'у
chain.doFilter(request, response);
// "После" — выполняется, когда обработка запроса ниже по цепочке завершилась
System.out.println("Filter: after controller");
}
}
Две важные детали. Во‑первых, 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 работает по схеме: «поймал запрос — нашёл handler — вызвал handler — превратил результат в ответ». И для нашей лекции здесь важнее всего одно: DispatcherServlet живёт после фильтров. Всё, что мы обсуждали в предыдущем разделе, может произойти ещё до MVC.
Чтобы почувствовать это руками, достаточно иметь простой контроллер. Например, наш endpoint личной зоны:
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") // Endpoint "личной зоны": на нём удобно наблюдать, блокирует ли нас security
public String me() {
// Маркер: если эта строка не печатается, значит запрос не дошёл до контроллера
System.out.println("Controller method called");
return "me"; // Учебный ответ
}
}
Если вы делаете запрос и эта строка не выводится, это не значит, что у вас «сломался контроллер». Это значит, что запрос до него не дошёл. Причины бывают разные, но в security‑курсе мы почти всегда сначала подозреваем фильтры и цепочку безопасности.
С этого момента полезно выработать привычку: когда видите «странное поведение endpoint’а», сначала спросите себя: «А запрос вообще дошёл до DispatcherServlet?» Если нет — смотрим фильтры и security.
4. Spring Security в filter chain
Теперь соберём всё в одну картинку. 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 (как часть filter chain)"]
DS["DispatcherServlet (Spring MVC)"]
Controller["Controller method"]
Response["HTTP response"]
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. Мини‑эксперимент: путь запроса
Сейчас будет маленький эксперимент, чтобы вы не просто прочитали теорию, а увидели причинно‑следственную связь. Мы не будем в этой лекции писать security-конфигурацию (это будет позже), но можем сами «подсветить» pipeline: фильтр пишет сообщения, контроллер пишет сообщения, а вы сравниваете, что произошло на самом деле.
Если вы добавили TraceFilter и MeController, то у вас появляются три возможных сценария:
Сценарий A: запрос не дошёл до контроллера из‑за security (например, вы не аутентифицировались). Тогда вы, скорее всего, увидите только вывод фильтра «before», а “Controller method called” — нет. Иногда вы увидите ещё и “after” (если цепочка вернула управление обратно), но самого контроллера не будет.
Сценарий B: запрос дошёл до контроллера (например, вы сделали запрос с правильными учётными данными для default user из 2‑го дня). Тогда вы увидите:
- “Filter: before controller”
- “Controller method called”
- “Filter: after controller”
Это и будет доказательством, что контроллер действительно находится в середине цепочки, а не стоит «первым».
Сценарий C: запрос не дошёл до контроллера, потому что его остановил «свой» фильтр (например, BlockMeFilter). Тогда вы тоже увидите, что контроллер молчит, но причина будет не в Spring Security, а в вашем учебном блокаторе. Это полезно: вы начинаете отличать «не дошло до MVC» от «дошло, но упало в контроллере».
Чтобы было проще сверять ожидания, вот типичная логика вывода (псевдо‑пример; не гарантия до байта, но по смыслу так):
Filter: before controller
Filter: after controller
Если контроллер не вызвался, строки “Controller method called” нет. И это уже ценная информация.
6. Типичные ошибки при работе с filter chain
Ошибка №1: искать причину отказа только в контроллере.
Когда вы видите 401, 403, редирект на логин или «в Postman работает иначе», очень легко начать править @GetMapping, DTO и сервисы. Но если запрос не дошёл до DispatcherServlet, все эти правки — как чинить дверную ручку в комнате, в которую вас не пустили охранники.
Ошибка №2: думать, что фильтр — это только «логирование для взрослых».
На практике фильтр — это точка, которая может полностью изменить судьбу запроса. Он может остановить обработку, выставить статус, добавить заголовки, сделать редирект, не пустить запрос в MVC. И Spring Security именно так и работает: он не «советует контроллеру», он действует раньше.
Ошибка №3: путать DispatcherServlet и контроллер.
Контроллер — это ваш код с @RestController. DispatcherServlet — инфраструктурный вход в MVC. Между ними много механики. Если вы их не различаете, то неизбежно начнёте думать «Spring MVC = контроллер», а это снова возвращает к ложной модели «URL → метод».
Ошибка №4: пытаться объяснить всю безопасность аннотациями на endpoint’ах.
Аннотации на контроллере важны, но они не отвечают на вопрос «когда вообще этот метод вызовется». Если security остановил запрос в фильтрах, никакая аннотация «внутри MVC» не поможет — потому что MVC до неё просто не дошёл.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ