1. @PreAuthorize без method security
Когда правило уже хочется повесить не на /api/editor/**, а прямо на publish(...), рука почти сразу тянется к @PreAuthorize.
Есть почти универсальная история: студент находит аннотацию @PreAuthorize, ставит её на метод сервиса… и ничего не происходит. Метод вызывается, данные меняются, публикация проходит, админка «случайно» доступна не тому человеку — и возникает ощущение, что Spring Security «не уважает» ваши аннотации. На самом деле он их уважает, просто он их пока не читает.
В Spring Security есть два больших мира. Первый — это web security: фильтры, SecurityFilterChain, AuthenticationEntryPoint, AccessDeniedHandler, вся эта красота, которая срабатывает до попадания запроса в контроллер. Второй — method security: проверка доступа на уровне вызова метода Spring-бины. И второй мир по умолчанию не активируется только от факта, что вы добавили spring-boot-starter-security.
Если сказать совсем по-человечески: @PreAuthorize — это “правило”, но правила сами себя не исполняют. Нужен механизм, который перед выполнением метода скажет: «Стоп, проверим, можно ли сюда заходить». Этот механизм включается аннотацией @EnableMethodSecurity.
Мини-демонстрация «как можно себя обмануть» выглядит примерно так: вы пишете сервис…
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class DemoService {
// Эта аннотация начнёт реально работать только после включения method security через @EnableMethodSecurity
@PreAuthorize("denyAll()")
public void doSensitiveStuff() {
// Важно: если method security не включён, этот метод всё равно выполнится,
// потому что Spring не будет перехватывать вызов и проверять правило доступа.
}
}
…а потом удивляетесь, что всё «работает». Да, будет работать — потому что @PreAuthorize без @EnableMethodSecurity в большинстве случаев остаётся просто декоративной надписью. И что особенно неприятно: приложение может не ругаться и не предупреждать. Оно просто будет делать вид, что аннотации не существует.
2. Два уровня: filter chain и method security
Чтобы не превратить безопасность в кашу, полезно прямо в голове разделить две границы. Первая граница — это «вход в приложение по HTTP». Вторая — это «вход в бизнес-операцию». И да, иногда эти границы совпадают (один endpoint → один метод сервиса), но часто они расходятся, особенно когда появляются повторное использование сервисов, разные контроллеры, batch-операции, внутренние вызовы и прочие «радости реального мира».
Схема, которую стоит держать перед глазами, такая:
flowchart TD
C[Client] -->|HTTP request| F[SecurityFilterChain]
F -->|если прошёл| Ctrl[Controller]
Ctrl -->|вызов метода| P[Service Proxy]
P -->|если прошёл| S[Service method]
Фильтры в SecurityFilterChain принимают решение, можно ли запросу вообще пройти дальше: пустить ли анонимного в public-зону, требовать ли аутентификацию, какая роль подходит для /api/admin/** и так далее.
Method security включается позже: когда контроллер уже вызвал сервис. Тут проверка происходит перед выполнением метода (например, publish(...)), и уже не по URL, а по текущему Authentication в SecurityContext и правилам на методе.
Обычно удобно зафиксировать разницу в табличке, чтобы не путаться:
| Вопрос | Request-level (через SecurityFilterChain) | Method-level (через @PreAuthorize) |
|---|---|---|
| На что смотрим | URL, HTTP-метод, headers, matchers | На вызов метода и текущего пользователя |
| Где живёт | В конфиге Security () |
На сервисном методе (и немного в конфиге через @EnableMethodSecurity) |
| Когда срабатывает | До контроллера | Перед методом Spring-бины |
| Типичная задача | «Кто может зайти в /api/admin/**?» | «Кто может выполнить lockUser()?» |
| Что будет, если забыть | Endpoint случайно открыт/закрыт | Критический метод доступен из неожиданных мест |
И вот ключевой вывод: SecurityFilterChain у нас уже есть (мы сделали его раньше), а method security нужно явно включить, иначе аннотации на методах никто не будет исполнять.
3. Роль @EnableMethodSecurity
Когда вы добавляете @EnableMethodSecurity, Spring начинает на старте приложения поднимать инфраструктуру, которая умеет перехватывать вызовы методов и проверять, есть ли на них security-аннотации (в нашем курсе — прежде всего @PreAuthorize). Под капотом там действительно много деталей, но на данном шаге нам важно понять практическую идею: Spring «подкладывает» проверку доступа до выполнения метода.
Если вы любите аналогии (а кто не любит, кроме людей, которые пишут regex с первого дубля), то @EnableMethodSecurity — это как включить сигнализацию в доме. Пока сигнализация выключена, вы можете повесить на дверь хоть десять табличек «Осторожно, охрана», но они не кусаются. Когда сигнализация включена — любая попытка «войти не туда» приводит к реакции системы.
Ещё один важный момент: starter-security включает web security «из коробки», потому что без него приложение было бы слишком легко случайно оставить открытым. А method security не включают по умолчанию, потому что он влияет на вызовы методов внутри приложения, то есть становится глубокой кросс-срезной частью. Он включает AOP/прокси-механику и меняет путь вызова внутри приложения, поэтому это решение должно быть осознанным.
На практике в нашем курсе всё упирается в одну строку в конфигурации — и это приятно.
4. Включение method security в проекте
Хорошая новость: технически method security включается очень коротким классом. Плохая новость: именно из-за этой короткости его чаще всего забывают.
В нашем проекте удобнее всего завести отдельный конфигурационный класс в пакете com.example.securecontent.security.config, чтобы он лежал рядом с SecurityFilterChain и не прятался где-то в случайном месте.
package com.example.securecontent.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity // Включаем обработку аннотаций вроде @PreAuthorize на методах Spring-бинов
public class MethodSecurityConfig {
// Нам не нужны @Bean-методы: достаточно «включателя» инфраструктуры
}
На этом этапе важно две вещи.
Во-первых, этот класс должен попасть в component scan Spring Boot. Если ваш главный класс приложения находится в com.example.securecontent, а конфиг — в com.example.securecontent.security.config, то всё хорошо: Spring Boot просканирует под-пакеты, класс увидится и аннотация сработает.
Во-вторых, не путайте @EnableMethodSecurity с настройкой web security. У вас по-прежнему должен быть SecurityFilterChain. Он никуда не девается. Method security — это не «замена» фильтрам, а второй уровень, который начинает работать на уровне сервисов.
Например, ваш web-конфиг может выглядеть так (упрощённо, чтобы видеть идею):
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class WebSecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Правила на уровне HTTP-запросов: срабатывают до попадания в контроллер
http.authorizeHttpRequests(auth -> auth
// Публичная зона доступна всем (включая анонимных)
.requestMatchers("/api/public/**").permitAll()
// Всё остальное требует аутентификацию (но это ещё не «проверка прав на бизнес-операцию»)
.anyRequest().authenticated()
);
// Важно: именно build() собирает SecurityFilterChain, который будет применён к запросам
return http.build();
}
}
Да, здесь мы сделали очень широкое правило anyRequest().authenticated(). Такой конфиг нарочно грубый: нам нужно, чтобы web-слой просто пропустил аутентифицированного пользователя дальше, а method-level правило уже само остановило лишний вызов. Это не новая финальная карта доступа проекта. В реальном проекте вы уже научились делать более точные matchers для /api/editor/** и /api/admin/**, но такой упрощённый вариант удобен, чтобы проверить, что method security действительно включилась и начала «кусаться» там, где вы ожидаете.
И вот только когда method security включён, аннотации на методах перестают быть декорацией.
5. Проверка работы method security и 401/403
Самый честный способ проверить, что вы всё подключили правильно, — защитить один метод сервиса и попробовать вызвать его тем пользователем, у которого нужного права нет. Идеально, если ваш request-level rule пропускает запрос (например, требует лишь аутентификацию), а method-level rule уже его остановит. Тогда вы увидите, что два уровня реально дополняют друг друга.
Представим, что у нас есть сервис редакторских операций, и мы хотим защитить очередь на ревью по authority 'draft:review'.
package com.example.securecontent.content;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class ReviewService {
// Проверка именно права (authority) на выполнение бизнес-операции
@PreAuthorize("hasAuthority('draft:review')")
public void loadQueue() {
// Здесь будет бизнес-логика загрузки очереди.
// Если authority нет, до этого места выполнение не дойдёт (будет 403 при вызове через web).
}
}
И контроллер, который просто делегирует:
package com.example.securecontent.content;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EditorReviewController {
private final ReviewService reviewService;
// Внедряем сервис через конструктор: так Spring создаст бин, и method security сможет работать корректно
public EditorReviewController(ReviewService reviewService) {
this.reviewService = reviewService;
}
@GetMapping("/api/editor/review-queue")
public void reviewQueue() {
// Контроллер ничего не решает про права: он просто вызывает сервисный метод,
// а доступ контролирует @PreAuthorize на уровне method security.
reviewService.loadQueue();
}
}
Что произойдёт теперь?
Если вы не включили @EnableMethodSecurity, то @PreAuthorize(...) не будет исполняться, и любой аутентифицированный пользователь пройдёт (при условии, что request-level правила его пропустили).
Если вы включили @EnableMethodSecurity, то при вызове loadQueue() Spring сделает проверку authority. И если authority нет, вы получите 403 Forbidden. Это очень важный момент: пользователь уже аутентифицирован (иначе было бы 401), но права на конкретную операцию нет.
И здесь полезно связать это с уже настроенной REST-моделью ошибок: ваш AccessDeniedHandler для REST API будет обрабатывать именно такие случаи и отдавать предсказуемый JSON-ответ. То есть method security автоматически вписывается в уже собранную схему ошибок: вам не нужно городить обработку AccessDeniedException в каждом контроллере.
6. Размещение @EnableMethodSecurity в конфиге
Когда вы решаете «куда положить» @EnableMethodSecurity, есть два типичных пути, и оба технически корректны. Разница — в читаемости и в том, как легко будет ориентироваться в проекте через две недели (а через месяц вы и сами начнёте подозревать, что в код ночью кто-то ставит аннотации за вас).
Первый путь — поставить @EnableMethodSecurity на тот же класс, где у вас объявлен SecurityFilterChain. Плюс в том, что всё security-собрано в одном месте. Минус в том, что такой класс очень быстро становится «секьюрити-комбайном»: и фильтры, и exception handling, и CORS/CSRF, и method security — всё в одну корзину.
Второй путь — держать включение method security отдельным маленьким конфигом, как мы сделали выше (MethodSecurityConfig). Этот подход мне нравится в учебном проекте: он подчёркивает, что method security — отдельный слой. И если у студента вдруг @PreAuthorize не работает, можно быстро сказать: «Проверь, есть ли у тебя MethodSecurityConfig и точно ли он подхватился».
Ещё один важный стильный момент: не стоит ставить @EnableMethodSecurity на главный класс приложения (@SpringBootApplication). Да, оно будет работать. Но получится «архитектурный клей»: ваш главный класс станет местом, где свалены разные “включалки”, которые не относятся к бизнесу приложения. В учебном проекте мы стараемся держать такие решения в пакетах security/config, чтобы «переключатели» жили рядом.
7. Типичные ошибки при включении method security
Ошибка №1: добавили @PreAuthorize, но забыли @EnableMethodSecurity.
Это самый классический сценарий. В итоге приложение ведёт себя так, будто аннотации нет, и новичок начинает «чинить» всё подряд: настройки фильтров, роли, matchers, обработчики ошибок. Правильная диагностика начинается с простого: есть ли в приложении класс с @EnableMethodSecurity, и точно ли он подхватывается Spring Boot.
Ошибка №2: конфигурационный класс лежит вне component scan.
Если вы положили MethodSecurityConfig в пакет, который не сканируется Spring Boot (например, в соседний root-пакет или в модуль, который не подключён), @EnableMethodSecurity просто не сработает. Внешне это выглядит так же, как ошибка №1: аннотации «игнорируются». Поэтому держите конфиги внутри базового пакета приложения, например com.example.securecontent.security.config.
Ошибка №3: пытаетесь защищать метод в объекте, который создан не Spring.
Method security работает на Spring-бинах. Если вы где-то сделали new ReviewService(...) (или создали сервис вручную в тестовом коде без контекста), никаких прокси и никаких проверок не будет. В учебном проекте это легко избежать: сервисы всегда должны быть @Service, а зависимости приходить через конструктор.
Ошибка №4: переносите правила в method security и удаляете request-level правила полностью.
Иногда студент думает: «Раз есть @PreAuthorize, значит фильтры мне больше не нужны». Это опасный перекос. Request-level защита остаётся первой линией обороны: она быстро отсеивает анонимных, защищает технические зоны (/api/admin/**), даёт предсказуемую карту доступа по URL. Method security усиливает её и закрывает бизнес-дырки, но не заменяет.
Ошибка №5: начинаете аннотировать всё подряд, включая внутренние “helper” методы.
Method security лучше всего работает, когда правила стоят на понятных публичных entry methods сервиса, где реально начинается бизнес-операция. Если вы начнёте размечать аннотациями каждый второй приватный или маленький метод, конфигурация станет шумной, а часть проверок будет вести себя неожиданно (почему именно — мы подробно разберём через прокси и self-invocation в следующей лекции этого дня).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ