JavaRush /Курсы /Spring Security /Spring Security в request lifecycle

Spring Security в request lifecycle

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

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 до неё просто не дошёл.

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