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. Сьогодні ми спеціально тримаємо фокус на правилах доступу і вчимося не змішувати ці два сенси в голові.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ