JavaRush /Курси /Spring Security /authorizeHttpRequests...

authorizeHttpRequests: базові правила

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

1. authorizeHttpRequests: правила замість if-ів

Якщо ви коли-небудь намагалися «захистити API нашвидкуруч», то майже напевно доходили до чогось на кшталт if(user == null) return 401; просто в контролері. Перші пʼять хвилин це навіть працює — доки не зʼявляються другий контролер, третій endpoint, «адмінка», «публічна зона», а потім ще й раптова потреба повторювати ті самі правила в кількох місцях. У підсумку security перетворюється на хаотичний шар.

Spring Security пропонує інший спосіб мислити про доступ. Замість того щоб писати перевірки в коді бізнес-логіки, ми описуємо політику доступу до запитів окремою декларативною мовою. По-людськи це звучить як короткі фрази: «усе вимагає входу», «публічне доступне всім», «особисте — лише тим, хто увійшов». Саме тут і зʼявляється authorizeHttpRequests.

Важливо вловити ідею: authorizeHttpRequests — це не «магічна кнопка безпеки», а спосіб перетворити доступ на набір правил, який можна прочитати згори вниз. А якщо правило читається, його набагато складніше зламати випадковою правкою (хоча… програмісти — народ творчий, тож шанс лишається).

2. authorizeHttpRequests(...): де живуть правила і як їх читати

Коли ви вперше бачите authorizeHttpRequests, мозок може відреагувати приблизно так: «Ага, ще одна ланка методів, зараз буде біль». Але якщо прибрати пафос, це просто блок конфігурації: «ось тут ми описуємо, хто має право виконувати HTTP-запити». Це про authorization, а не про те, як саме користувач входить у систему і звідки береться пароль.

На практиці це виглядає так: у нас є SecurityConfig, усередині — SecurityFilterChain, і ми додаємо один рядок, який вмикає режим «правила доступу задаємо ми». Далі всередині лямбди auth -> ... будемо писати самі правила.

Мінімальний робочий фрагмент, який уже задає сенс (і який зручно тримати як стартову точку), виглядає так:

// Усередині вже зібраного SecurityFilterChain змінюється ось цей блок:
http.authorizeHttpRequests(auth -> auth
    // Правило «за замовчуванням»: усе має вимагати входу
    .anyRequest().authenticated()
);

Зараз не чіпляйтеся до anyRequest() і authenticated() — ми докладно розберемо їх просто в цій лекції. Поки важливіше побачити загальну форму: authorizeHttpRequests приймає лямбду, усередині якої ми складаємо правила. І далі вам потрібно навчитися буквально проговорювати це як речення: «будь-який запит має бути аутентифікований».

Щоб закріпити, корисно тримати маленьку «шпаргалку-перекладач»:

Фрагмент DSL Як це читати по-людськи Про що це
authorizeHttpRequests(...) «Налаштовуємо правила доступу до запитів» Авторизація
anyRequest() «Усе, що лишилося» Матчинг запитів
permitAll() «Пустити всіх» Дозвіл без умов
authenticated() «Пустити лише тих, хто увійшов» Вимога аутентифікації

У наступних лекціях ми розберемо точніші правила для конкретних URL, але вже зараз ви маєте впевнено розуміти базову мову: «який запит» → «яка умова».

3. Базові правила доступу

anyRequest() — ловимо «усе інше» одним правилом

Коли ми описуємо правила доступу, одразу виникає запитання: а що робити із запитами, які ми явно не перелічили? Їх можна ігнорувати, сподіватися на диво, поставити свічку за стабільність продакшена — але краще зробити професійно: визначити загальне правило, яке спрацьовує на все, що не підійшло під точніші умови. Це і є anyRequest().

anyRequest() читається дуже буквально: «будь-який запит». На практиці це означає «будь-який запит, для якого не знайшлося точнішого правила вище». Навіть якщо сьогодні у вас лише одне правило, звичка думати «а чим закінчується політика?» — це вже половина якості security-конфігурації.

Є ще одна важлива думка: anyRequest() за змістом має бути останнім. Це як фраза «взагалі для всіх» — якщо ви вимовили її першою, далі вже немає сенсу щось уточнювати. У Spring Security це не просто філософія: anyRequest() фактично завершує побудову набору правил, і спроби додати щось «після» зазвичай закінчуються помилкою конфігурації або недосяжними правилами.

Найпростіший приклад, який задає нижню межу безпеки, виглядає так:

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

// Налаштовуємо правила авторизації запитів
http.authorizeHttpRequests(auth -> auth
    // "Будь-який запит" (який не збігся з точнішими правилами) має бути від аутентифікованого користувача
    .anyRequest().authenticated()
);

Сенс простий: якщо ми забули щось описати окремо, воно все одно залишиться закритим для анонімних. Це дуже близько до принципу secure-by-default, який ми обговорювали раніше: краще випадково закрити зайве, а потім свідомо відкрити його, ніж випадково відкрити зайве й потім пояснювати на ретроспективі, чому у вас /api/admin/users був публічним.

permitAll() — «публічно» не означає «безпечно»

permitAll() — це найпростіше правило, і саме тому з ним легко помилитися. Воно означає: «дозволити доступ усім, незалежно від того, увійшов користувач чи ні». Якщо ви читаєте це як «дозволити всім аутентифікованим» — це не permitAll, це вже радше authenticated. Тут усе буквально: permit — «дозволити», all — «усім».

Звучить так, ніби permitAll вимикає Spring Security. Насправді ні: Spring Security продовжує жити в застосунку, фільтри продовжують відпрацьовувати, SecurityContext продовжує існувати (просто там буде анонімний користувач, якщо ніхто не увійшов). permitAll — це саме рішення на рівні авторизації: «не вимагай жодних умов».

Найбільш відкритий базовий варіант виглядає так:

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

// Налаштовуємо правила авторизації запитів
http.authorizeHttpRequests(auth -> auth
    // Повністю публічний режим: пускаємо всіх, навіть анонімних
    .anyRequest().permitAll()
);

І це корисно як контрастний експеримент. Наприклад, ви хочете перевірити, що endpoint узагалі працює, і відокремити проблему «у мене контролер не піднявся» від проблеми «мене не пускає security». У такому режимі застосунок знову стає ніби без охорони.

Але важливий професійний момент: permitAll на anyRequest() — це не рішення, а максимум тимчасова діагностична позиція. У реальному backend-проєкті тримати все публічним — приблизно як повісити на сервер табличку «пароль: admin». Можливо, чесно, але краще все ж не треба.

authenticated() — «увійшов у систему» без ролей і прав

Слово authenticated() звучить грізно, але за змістом воно дуже базове: «пустити тільки ті запити, де користувач аутентифікований». Тобто система має розуміти, хто робить запит, і вважати його не анонімним. При цьому authenticated() не перевіряє, який саме це користувач, не перевіряє роль, не перевіряє «чи можна йому публікувати статті» і не перевіряє «це його чорновик чи чужий». Це просто барʼєр «спочатку назвіть себе».

Дуже корисна аналогія, хоч і побутова: authenticated() — це турнікет за перепусткою. Він перевіряє, що перепустка є, але не перевіряє, в яку саме кімнату вам можна. Для кімнат будуть інші механізми, але сьогодні ми вчимося вмикати хоча б турнікет.

У коді це виглядає так:

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

// Налаштовуємо правила авторизації запитів
http.authorizeHttpRequests(auth -> auth
    // Пускаємо тільки запити, де користувач уже не анонімний
    .anyRequest().authenticated()
);

І ось тут важливо повʼязати це з тим, що ми вже знаємо з попередніх днів. Spring Security зазвичай відновлює Authentication у SecurityContext. Якщо запит анонімний, усередині часто буде AnonymousAuthenticationToken (ми це обговорювали в контексті SecurityContext). authenticated() якраз означає: «анонімний не підходить, потрібен справжній користувач».

Ще один частий підводний камінь новачка: authenticated() — це не перевірка логіна й пароля саме в цій точці. Перевірка логіна й пароля відбувається в механізмі аутентифікації (filters + AuthenticationManager + providers). А authenticated() — це вже про результат: «чи має запит стан “користувач підтверджений”».

Тобто спочатку десь раніше в ланцюжку (або раніше в часі) має відбутися аутентифікація, і тільки потім authenticated() може сказати: «так, користувач увійшов, пускаємо». Якщо аутентифікація не відбулася, Spring Security відповідає відмовою (і ви бачите 401, редирект на логін та інші радості, які ми вже спостерігали в Day 2).

4. Два базові режими: «усе закрито» і «усе відкрито»

На цьому етапі корисно зробити те, що люблять інженери: взяти два крайні режими й подивитися, як у них поводиться система. Один режим — «за замовчуванням усе закрито для анонімних», другий — «за замовчуванням усе відкрито для всіх». У реальному продукті обидва рідко бувають фінальними, але для тренування читання правил вони ідеальні.

Давайте візьмемо наш навчальний проєкт Secure Content Platform API. У нього є зони, які за змістом мають бути публічними (наприклад, читання опублікованих статей), і зони, які мають бути особистими або привілейованими (наприклад, /api/me, редакторські та admin-операції). Поки ми не вміємо вибирати конкретні URL правилами — це буде наступна лекція. Але вже зараз можемо побачити, як працює «сітка безпеки».

Щоб експеримент був наочнішим, додамо найпростіший endpoint для перевірки (якщо він у вас уже є — чудово, просто використайте наявний). Наприклад, маленький «пінг»:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PingController {

    // Простий endpoint для перевірки: відповідає "pong", щоб було видно, що запит дійшов до контролера
    @GetMapping("/api/ping")
    public String ping() {
        return "pong";
    }
}

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

Тепер розглянемо два варіанти.

Варіант А: «усе вимагає входу».
Ми ставимо загальний барʼєр:

// Базова політика: будь-який запит вимагає аутентифікації
http.authorizeHttpRequests(auth -> auth
    .anyRequest().authenticated()
);

У такому режимі будь-який запит без аутентифікації буде відхилено. Чим саме — залежить від клієнта, з якого ви звертаєтеся (браузер, Postman, curl), і від того, які механізми аутентифікації зараз увімкнені. За замовчуванням Spring Security часто показує сторінку входу в браузері та/або пропонує basic challenge клієнтам. Але сенс один: «анонімно не можна».

Якщо ви перевіряєте через curl, логіка зазвичай виглядає так:

# Запит без облікових даних — анонімний
curl -i http://localhost:8080/api/ping
# Очікувано: 401 Unauthorized (або редирект на /login, якщо ви дивитеся це як браузер)

А якщо ви вкажете облікові дані (наприклад, користувача за замовчуванням, якого Spring Boot створював раніше), то запит пройде:

# Запит із basic auth — користувач представився
curl -i -u user:password http://localhost:8080/api/ping
# HTTP/1.1 200
# pong

(Пароль, звісно, буде не password, а той самий згенерований, який ви бачили в логах під час попереднього запуску застосунку.)

Варіант Б: «усе публічне».
Змінюємо один рядок:

// Повністю публічна політика: будь-який запит дозволено без умов
http.authorizeHttpRequests(auth -> auth
    .anyRequest().permitAll()
);

І тепер запит без аутентифікації спокійно проходить:

# Тепер навіть анонімний запит проходить, тому що політика permitAll
curl -i http://localhost:8080/api/ping
# HTTP/1.1 200
# pong

Чому обидва варіанти погані як фінал? Тому що проєкт за своєю природою неоднозонний. У ньому одночасно є публічне читання та приватні/привілейовані дії. Якщо «усе закрито», ви ламаєте публічну частину й робите платформу контенту дивною: «щоб прочитати статтю — увійдіть». Якщо «усе відкрито», ламаєте безпеку: «щоб керувати користувачами — просто перейдіть за посиланням».

І саме тому нам потрібна мова, яка вміє сказати: «ось цей шматок публічний, а ось цей — лише для тих, хто увійшов». Сьогодні ми вивчили слова permitAll() і authenticated(). У наступній лекції ми навчимося застосовувати ці слова до конкретних URL. Поки ж важливо не плутати зміст цих операторів і вміти прогнозувати поведінку застосунку в крайніх режимах.

5. Типові помилки під час налаштування правил доступу

Помилка № 1: плутати permitAll() і «дозволити після входу».
Новачок іноді думає, що permitAll — це «пустити всіх користувачів системи». Але насправді це «пустити взагалі всіх, включно з анонімними». Це критично в проєкті, де є admin-endpoints. З такою плутаниною можна випадково залишити привілейовану операцію публічною, і це буде не баг, а катастрофа — просто дуже швидка.

Помилка № 2: вважати authenticated() перевіркою ролі.
authenticated() не знає нічого про ролі ADMIN або EDITOR. Він перевіряє лише факт «користувач не анонімний». Через це іноді виникають хибні очікування: «чому звичайний користувач пройшов туди, куди не можна?» Відповідь: тому що ви вимагали лише authenticated(), а не суворіших правил. Ролі й authorities зʼявляться пізніше, але вже зараз важливо чітко відділяти «увійшов» від «має право».

Помилка № 3: використовувати anyRequest().permitAll() як тимчасовий «фікс» і забути повернути назад.
Це класика розробки: «мені заважає security, вимкну на хвилинку», а потім минає три дні, ви робите коміт, пушите — і раптом у вас у репозиторії «хвилинка» стає постійною. Якщо вже ви тимчасово відкриваєте все заради діагностики, робіть це дуже свідомо й повертайте назад одразу після перевірки.

Помилка № 4: не сприймати конфігурацію як текст правил.
Якщо ви дивитеся на authorizeHttpRequests як на «ну це такий DSL, я його не розумію, просто скопіюю», то далі буде важко: зʼявляться кілька зон, винятки, порядок правил, і ви почнете ламати систему випадковими рухами. Звичка проговорювати кожен рядок («для цих запитів така умова») здається занудною, але вона рятує від магії.

Помилка № 5: забувати, що authorization — це окреме завдання від authentication.
Іноді очікують, що коли написали authenticated(), Spring Security «якось сам зрозуміє, де логін». Насправді authenticated() — це вимога, а не механізм входу. Механізм аутентифікації живе в інших частинах Spring Security. Сьогодні ми спеціально тримаємо фокус на правилах доступу і вчимося не змішувати ці два сенси в голові.

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