JavaRush /Курси /Spring Security /Method security і @EnableMe...

Method security і @EnableMethodSecurity

Spring Security
Рівень 19 , Лекція 1
Відкрита

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». Друга — це «вхід до бізнес-операції». І так, інколи ці межі збігаються (одна кінцева точка → один метод сервісу), але часто вони розходяться, особливо коли з’являються повторне використання сервісів, різні контролери, пакетні операції, внутрішні виклики та інші радощі реального світу.

Схема, яку варто тримати перед очима, така:

flowchart TD
    C[Клієнт] -->|HTTP-запит| F[SecurityFilterChain]
    F -->|якщо пройшло| Ctrl[Контролер]
    Ctrl -->|виклик методу| P[Проксі сервісу]
    P -->|якщо пройшло| S[Метод сервісу]

Фільтри в SecurityFilterChain ухвалюють рішення, чи може запит взагалі пройти далі: чи пускати анонімного в публічну зону, чи вимагати автентифікацію, яка роль підходить для /api/admin/** і так далі.

Method security вмикається пізніше: коли контролер уже викликав сервіс. Тут перевірка відбувається перед виконанням методу (наприклад, publish(...)), і вже не за URL, а за поточним Authentication у SecurityContext та правилами на методі.

Зазвичай зручно зафіксувати різницю в таблиці, щоб не плутатися:

Питання Рівень запиту (через SecurityFilterChain) Рівень методу (через @PreAuthorize)
На що дивимося URL, HTTP-метод, заголовки, умови зіставлення На виклик методу й поточного користувача
Де розміщено У конфігу Security (
@Bean SecurityFilterChain
)
На сервісному методі (і трохи в конфігу через @EnableMethodSecurity)
Коли спрацьовує До контролера Перед методом Spring-біна
Типова задача «Хто може зайти в /api/admin/** «Хто може виконати lockUser()
Що буде, коли забути Кінцева точка випадково відкрита або закрита Критичний метод доступний з неочікуваних місць

І ось ключовий висновок: SecurityFilterChain у нас уже є (ми зробили його раніше), а method security потрібно явно увімкнути, інакше анотації на методах ніхто не виконуватиме.

3. Роль @EnableMethodSecurity

Коли ви додаєте @EnableMethodSecurity, Spring починає на старті застосунку піднімати інфраструктуру, яка вміє перехоплювати виклики методів і перевіряти, чи є на них security-анотації (у нашому курсі — насамперед @PreAuthorize). Під капотом там справді багато деталей, але на цьому кроці нам важливо зрозуміти практичну ідею: Spring «підкладає» перевірку доступу до виконання методу.

Якщо ви любите аналогії, то @EnableMethodSecurity — це як увімкнути сигналізацію в будинку. Поки сигналізацію вимкнено, ви можете повісити на двері хоч десять табличок «Обережно, охорона», але вони не кусаються. Коли сигналізацію ввімкнено — будь-яка спроба «зайти не туди» призводить до реакції системи.

Ще один важливий момент: starter-security вмикає web security «з коробки», тому що без нього застосунок було б занадто легко випадково залишити відкритим. А method security не вмикають за замовчуванням, бо він впливає на виклики методів усередині застосунку, тобто стає глибокою крос-функціональною частиною. Він вмикає AOP- і proxy-механіку та змінює шлях виклику всередині застосунку, тому це рішення має бути усвідомленим.

На практиці в нашому курсі все впирається в один рядок у конфігурації — і це приємно.

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-рівень просто пропустив автентифікованого користувача далі, а rule на рівні методу вже сам зупинив зайвий виклик. Це не нова фінальна карта доступу для проєкту. У реальному проєкті ви вже навчилися робити точніші matchers для /api/editor/** і /api/admin/**, але такий спрощений варіант зручний, щоб перевірити, що method security справді ввімкнулася і почала «кусатися» там, де ви цього очікуєте.

І лише тоді, коли method security увімкнено, анотації на методах перестають бути декорацією.

5. Перевірка роботи method security і 401/403

Найчесніший спосіб перевірити, що ви все підключили правильно, — захистити один метод сервісу й спробувати викликати його тим користувачем, у якого потрібного права немає. Ідеально, коли ваше правило на рівні запиту пропускає запит далі (наприклад, вимагає лише автентифікації), а правило на рівні методу вже його зупиняє. Тоді ви побачите, що два рівні справді доповнюють один одного.

Уявімо, що в нас є сервіс редакторських операцій, і ми хочемо захистити чергу на ревʼю за 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() {
        // Тут буде бізнес-логіка завантаження черги.
        // Якщо права немає, до цього місця виконання не дійде (буде 403 під час виклику через веб).
    }
}

І контролер, який просто делегує:

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(...) не виконуватиметься, і будь-який автентифікований користувач пройде далі, за умови що правила рівня запиту його пропустили.

Якщо ви увімкнули @EnableMethodSecurity, то під час виклику loadQueue() Spring зробить перевірку authority. І якщо authority немає, ви отримаєте 403 Forbidden. Це дуже важливий момент: користувач уже автентифікований (інакше було б 401), але прав на конкретну операцію немає.

І тут корисно пов’язати це з уже налаштованою REST-моделлю помилок: ваш AccessDeniedHandler для REST API оброблятиме саме такі випадки й повертатиме передбачувану JSON-відповідь. Тобто method security природно вписується в уже зібрану схему помилок: вам не потрібно городити обробку AccessDeniedException у кожному контролері.

6. Розміщення @EnableMethodSecurity у конфігу

Коли ви вирішуєте, «куди покласти» @EnableMethodSecurity, є два типові шляхи, і обидва технічно коректні. Різниця — у читабельності та в тому, як легко буде орієнтуватися в проєкті через два тижні (а через місяць ви й самі почнете підозрювати, що в коді вночі хтось ставить анотації за вас).

Перший шлях — поставити @EnableMethodSecurity на той самий клас, де у вас оголошено SecurityFilterChain. Плюс у тому, що все security зібрано в одному місці. Мінус у тому, що такий клас дуже швидко стає «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 і повністю видаляєте правила рівня запиту.
Іноді студент думає: «Раз є @PreAuthorize, значить фільтри мені більше не потрібні». Це небезпечний перекіс. Захист на рівні запиту лишається першою лінією оборони: він швидко відсікає анонімних, захищає технічні зони (/api/admin/**), дає передбачувану карту доступу за URL. Method security підсилює її й закриває бізнес-дірки, але не замінює.

Помилка № 5: починаєте анотувати все підряд, включно з внутрішніми “helper” методами.
Method security найкраще працює, коли правила стоять на зрозумілих публічних entry methods сервісу, де реально починається бізнес-операція. Якщо ви почнете розмічати анотаціями кожен другий приватний або дрібний метод, конфігурація стане шумною, а частина перевірок поводитиметься неочікувано (чому саме — ми детально розберемо через проксі та self-invocation у наступній лекції цього дня).

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ