formLogin + HttpSession vs HTTP Basic

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

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 і семантика сеансу, а не «один заголовок — і готово».

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