1. Порівняння моделей автентифікації
Коли вже зрозуміло, як Basic доходить до SecurityContext, природно виникає запитання: чим ця модель взагалі відрізняється від formLogin + HttpSession в одному й тому самому застосунку? Дуже легко потрапити в пастку «здається, я зрозумів одну схему — отже, вона завжди правильна». Особливо якщо схема спрацювала з першого разу, а в логах навіть не було червоного тексту (що за мірками backend — уже свято). Але formLogin + HttpSession і HTTP Basic розв’язують одне й те саме завдання — автентифікувати запит. Просто роблять це настільки по-різному, що вибір впливає на UX, налагодження і на те, як саме клієнт спілкується з API.
Порівняння в одному застосунку корисне з двох причин. По-перше, ви перестаєте сприймати Spring Security як набір магічних анотацій і починаєте бачити «контракт» між клієнтом і сервером: що саме клієнт надсилає, як сервер на це відповідає і що зберігається між запитами. По-друге, ви ще впевненіше розрізняєте authentication («хто ви?») та authorization («що вам можна?»), бо правила авторизації залишаються тими самими — змінюється лише спосіб входу.
2. Що не змінюється в застосунку
Найважливіший момент цього порівняння звучить майже нудно: коли ви замінюєте formLogin на HTTP Basic, то зазвичай не переписуєте бізнес-код. Контролери не мають «знати», чи ви увійшли через форму, чи через заголовок. Якщо контролер починає питати: «А ви точно через formLogin?» — це приблизно якби водій автобуса перевіряв, купили ви квиток у касі чи в застосунку. Автобусу байдуже, аби квиток був дійсний.
У нашому проєкті є чіткі зони: /api/public/** відкрита для всіх, /api/me потребує автентифікації, а /api/editor/** і /api/admin/** вимагають ролі. Ці правила живуть у SecurityFilterChain, а в контролері нам важливий лише поточний користувач (Principal), який уже з’явився після проходження ланцюжка безпеки.
Ось мінімальний приклад «особистої зони» — endpoint /api/me. Він однаково працює і з formLogin, і з HTTP Basic, бо Spring Security в обох випадках зрештою заповнює Principal.
package com.example.securecontent.profile;
import java.security.Principal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MeController {
@GetMapping("/api/me")
public String me(Principal principal) {
// На цей endpoint пускаємо лише автентифікованих користувачів,
// тому principal тут очікувано не null.
// (Якщо доступ випадково відкриють — зловите NPE і швидко зрозумієте, що правила зламано.)
return principal.getName(); // Повертаємо "хто я" — імʼя поточного користувача із SecurityContext
}
}
Важлива думка: ми не пишемо «вхід» вручну. Ми пишемо endpoint, який працює після того, як security-шар уже зробив свою роботу й поклав «хто я» в контекст запиту.
3. formLogin + HttpSession: відчуття «я увійшов»
formLogin хороший як навчальний міст, бо він візуальний: браузер справді показує сторінку входу, ви вводите логін і пароль, і далі здається, що «система вас запамʼятала». Це відчуття — не магія і не телепатія сервера, а цілком конкретний механізм: після успішної автентифікації сервер створює або оновлює сесію і надсилає клієнту cookie JSESSIONID. Браузер потім додає цю cookie до всіх наступних запитів, і сервер за нею знаходить ваш стан.
Якщо сказати без філософії, formLogin + HttpSession — це модель, де credentials передаються один раз (у момент входу), а далі клієнт носить із собою «браслет» (session id), який дає серверу змогу відновити SecurityContext. Усередині Spring Security це виглядає так: автентифікація відбулася, контекст збережено, далі запити просто впізнаються.
Найнаочніше це видно в послідовності запитів:
sequenceDiagram
%% formLogin: один раз входимо, далі "браслет" (JSESSIONID) ходить із нами сам
participant B as "Браузер"
participant S as "API платформи захищеного контенту"
B->>S: "GET /api/me (анонімно)"
S-->>B: "302 Перенаправлення на /login"
B->>S: "POST /login (імʼя користувача + пароль)"
S-->>B: "Set-Cookie: JSESSIONID=..."
B->>S: "GET /api/me + Cookie: JSESSIONID=..."
S-->>B: "200 OK (me = імʼя користувача)"
І тут важливо не переплутати: «браслет» (JSESSIONID) — це не ваш користувач і не ваша роль. Це лише ключ, за яким сервер знаходить збережений контекст безпеки.
4. HTTP Basic: «кожен запит приходить із паспортом»
HTTP Basic схожий на дуже суворий пропускний режим: ви не показуєте паспорт один раз на вході до будівлі, а пред’являєте його кожного разу, коли заходите до кабінету. Це не тому, що система шкідлива, а тому що модель така: credentials додаються до кожного захищеного запиту в заголовку Authorization.
У попередніх лекціях ми вже розглянули формат заголовка, тож тут нам важлива саме модель поведінки. За відсутності Authorization: Basic ... сервер не може автентифікувати запит і відповідає, що йому потрібні облікові дані. Це проявляється через 401 і заголовок WWW-Authenticate, який для клієнта означає: «спробуй ще раз, але вже з даними».
Типова послідовність має такий вигляд:
sequenceDiagram
%% httpBasic: автентифікація приїжджає в кожному запиті в заголовку Authorization
participant C as "API-клієнт (curl/Postman)"
participant S as "API платформи захищеного контенту"
C->>S: "GET /api/me (без Authorization)"
S-->>C: "401 + WWW-Authenticate: Basic ..."
C->>S: "GET /api/me + Authorization: Basic ..."
S-->>C: "200 OK (me = імʼя користувача)"
C->>S: "GET /api/admin/users + Authorization: Basic ..."
S-->>C: "403 Forbidden (якщо не ADMIN)"
Зверніть увагу на останній рядок: HTTP Basic відповідає лише на запитання «хто ви?». Запитання «що вам можна?» і далі вирішують ваші правила hasRole("ADMIN"), authenticated() і так далі.
5. Два варіанти SecurityFilterChain
Дуже спокусливо думати, що перехід на HTTP Basic — це «переписати півпроєкту». На практиці в навчальному проєкті це часто буквально один рядок у конфігурації. Нижче — два варіанти однієї й тієї самої карти доступу лише для порівняння поведінки клієнтів. У робочій конфігурації ви обираєте один механізм автентифікації, а не тримаєте два однакові ланцюжки поруч «про запас».
Нижче — два варіанти однієї й тієї самої конфігурації, де правила доступу однакові, а відрізняється лише механізм автентифікації.
Варіант A: орієнтований на браузер вхід через formLogin.
package com.example.securecontent.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfigFormLogin {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// Правила доступу (authorization): хто куди може ходити
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll() // Публічна зона
.requestMatchers("/api/admin/**").hasRole("ADMIN") // Зона лише для ADMIN
.anyRequest().authenticated() // Усе інше — лише після входу
)
// Механізм автентифікації (authentication): вхід через форму + сесія (JSESSIONID)
.formLogin(Customizer.withDefaults())
.build();
}
}
Варіант B: орієнтований на API вхід через HTTP Basic.
package com.example.securecontent.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfigHttpBasic {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// Ті самі правила доступу (authorization) — вони не змінюються
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll() // Публічна зона
.requestMatchers("/api/admin/**").hasRole("ADMIN") // Зона лише для ADMIN
.anyRequest().authenticated() // Усе інше — лише після входу
)
// Змінюється лише механізм автентифікації: тепер чекаємо Authorization: Basic ...
.httpBasic(Customizer.withDefaults())
.build();
}
}
Для порівняння корисно буквально зробити одну дію: тимчасово замінити .formLogin(...) на .httpBasic(...) і подивитися, як за тих самих кінцевих точок змінюється контракт із клієнтом.
6. Клієнти: браузер і curl/Postman
Коли порівнюють механізми автентифікації, новачки часто дивляться лише на рівень анотацій: увімкнули .formLogin() або .httpBasic() — і все. Але найчесніший спосіб порівняння — подивитися очима клієнта. Браузер любить перенаправлення та сторінки входу, а API-клієнти люблять статуси й заголовки. І Spring Security теж розмовляє з ними по-різному.
Нижче — компактна таблиця, яка допомагає не плутатися у відчуттях:
| Питання | formLogin + HttpSession | HTTP Basic |
|---|---|---|
| «Як я вперше доводжу, хто я?» | Через сценарій входу (login flow) | Через заголовок Authorization |
| «Що я надсилаю на кожен запит після входу?» | Зазвичай cookie JSESSIONID (браузер робить це сам) | Знову Authorization: Basic ... |
| «Що відбувається, якщо я не автентифікований?» | Часто перенаправлення на /login (особливо помітно в браузері) | 401 + WWW-Authenticate |
| «Як тестувати через curl?» | Незручніше: треба відтворювати семантику браузера | Дуже зручно: curl -u user:pass ... |
| «Як тестувати через браузер?» | Дуже природно | Може з’являтися системне basic-вікно (і це не завжди приємно) |
Щоб відчути різницю на практиці, достатньо двох команд curl для Basic. Перша — без облікових даних, щоб побачити 401:
# Запит без заголовка Authorization — сервер не може автентифікувати клієнта
curl -i http://localhost:8080/api/me
Друга — з обліковими даними, щоб побачити 200 і відповідь контролера:
# -u user:password змусить curl надсилати Authorization: Basic ...
curl -i -u user:password http://localhost:8080/api/me
У випадку formLogin ви зазвичай робите навпаки: вхід виконуєте через браузер, а потім просто ходите по /api/me та інших кінцевих точках, не думаючи про заголовки, бо браузер сам додає cookie. У цьому й полягає ключовий UX-ефект сесії: ви «увійшли», і далі взаємодія виглядає як один безперервний сеанс.
7. Компроміси: зручність, безпека, експлуатація
Порівняння formLogin і HTTP Basic легко перетворити на релігійну війну про те, що сучасніше. Але наш курс спеціально вчить іншого: обираємо механізм під клієнта і сценарій, а не за модою. У навчальному проєкті це особливо важливо: ви маєте бачити механіку, а не просто «запамʼятати правильну кнопку».
Якщо говорити чесно, formLogin + HttpSession чудово лягає в браузерну модель. Браузер уміє працювати з cookies, уміє переносити стан між запитами і підтримувати сеанс. З погляду сервера ви теж виграєте: пароль перевіряється в момент входу, а далі сервер найчастіше просто відновлює контекст за session id і виконує авторизацію. Це створює відчуття, що все працює швидко і плавно.
HTTP Basic, навпаки, чудово підходить як мінімальна базова модель для API-клієнта. Він робить автентифікацію максимально видимою: ось заголовок, ось схема, ось дані. Для Postman і curl це ідеальний формат, бо вони й так живуть заголовками. Його іноді називають stateless просто тому, що credentials приїжджають з кожним запитом. Але це ще не token-модель: клієнт і далі носить пароль, а не короткоживучий access token.
Але за це є очевидна плата: ви передаєте облікові дані в кожному запиті. Це означає, що дисципліна навколо HTTPS і логування стає не «хорошою практикою», а прямою обов’язковою гігієною, інакше ви самі випадково влаштуєте витік.
Можна ще простіше: formLogin — це «я показую документи один раз і отримую браслет», HTTP Basic — це «я показую документи щоразу». І в правильному контексті обидва підходи можуть бути інженерно адекватними.
Після такого порівняння запитання вже звучить не як «що краще взагалі», а як «для якого клієнта і якого сценарію який механізм доречний». І саме там у Basic починаються не лише плюси, а й цілком жорсткі межі.
8. Типові помилки: formLogin і HTTP Basic
Помилка №1: очікування, що .httpBasic() «дозволяє доступ».
«Увімкнули httpBasic» означає «додали спосіб автентифікувати запит», а не «зняли замки з усіх дверей». Правила hasRole("ADMIN") і authenticated() нікуди не зникають: користувач без потрібної ролі й далі отримає 403 на /api/admin/**.
Помилка №2: очікування «увійшов один раз і забув» від HTTP Basic.
У Basic-моделі клієнт або додає Authorization: Basic ... до кожного запиту, або вважається неавтентифікованим. Якщо в Postman один запит ви надіслали з авторизацією, а на наступному «забули вибрати той самий режим авторизації», ви побачите 401 — і це буде не «зламався сервер», а «клієнт прийшов без документів».
Помилка №3: логування заголовка Authorization заради налагодження.
Це спокусливо, бо заголовок такий зрозумілий, але саме тут налагодження перетворюється на витік: логи живуть довго й мандрують між людьми. Коли облікові дані приходять у кожному запиті, така «дрібна» звичка може стати великою проблемою.
Помилка №4: перенесення browser-очікувань на HTTP Basic.
Наприклад, чекати красиву сторінку входу. Basic — не про сторінки, він про заголовки (401, WWW-Authenticate, Authorization). Якщо вам потрібен UI-вхід — це вже територія formLogin (або іншого інтерактивного сценарію входу).
Помилка №5: перенесення очікувань HTTP Basic на formLogin під час тестування.
Очікувати, що formLogin буде зручний у curl «як Basic», — теж дивно. formLogin — механізм, орієнтований на браузер, і він не зобов’язаний бути приємним для консольного клієнта: там перенаправлення, cookies і семантика сеансу, а не «один заголовок — і готово».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ