JavaRush /Курси /Spring Security /Один endpoint, різні клієнти

Один endpoint, різні клієнти

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

1. Один endpoint і різні клієнти

Якщо ви тільки починаєте, дуже легко уявити собі просту картину світу: «Є URL, отже за ним або 200 OK, або “не працює”». Але Spring Security живе в реальному вебі, де клієнти помітно різняться. Браузер — це не просто HTTP-клієнт, а інструмент для навігації сторінками, який любить HTML, редиректи й форми входу. Postman і curl — це API-орієнтовані інструменти: для них важливіші статус і заголовки, ніж гарна сторінка входу.

Уявіть, що ваш бекенд — це клуб з охороною. Охорона одна й та сама, правила ті самі, але реакція залежить від того, хто підійшов. Якщо це людина, яка прийшла як відвідувач, охорона може сказати: «Проходьте до стійки реєстрації, там вхід за паспортом» — це схоже на редирект на login page. Якщо ж підійшов кур’єр і питає: «Де моя перепустка?», охорона відповідає коротко: «Покажіть перепустку» — це схоже на 401 Unauthorized. Клуб один. Охорона одна. Просто формат спілкування різний.

Саме тому важливо дивитися на один і той самий endpoint очима різних клієнтів. Якщо тестувати API лише браузером, дуже легко зробити хибний висновок: «Ой, воно повернуло HTML, отже API зламався». Хоча насправді все може працювати цілком нормально, а ви просто бачите браузерний сценарій входу.

2. Експеримент: protected endpoint без конфігурації

Щоб порівняння було чесним, нам потрібне дуже просте налаштування: один і той самий endpoint, один і той самий код, один і той самий застосунок, і єдина зовнішня відмінність — який клієнт робить запит. Ми спеціально не пишемо SecurityFilterChain, не додаємо правила доступу й не будуємо ролі. Ми просто дивимося на поведінку платформи за замовчуванням після підключення стартера.

У проєкті Secure Content Platform API у ролі такого індикатора чудово підходить особиста зона — endpoint /api/me. Сенс у нього людяний: «поверни щось про поточного користувача». Навіть якщо поки він повертає просто рядок, психологічно саме цей endpoint і має бути закритим.

Мінімальний контролер (спрощений навчальний варіант):

package com.example.securecontent.profile;

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

@RestController
public class CurrentUserController {

    // Захищена зона: сюди можна потрапити лише після автентифікації
    @GetMapping("/api/me")
    public String me() {
        // Якщо ви дійшли сюди — фільтри Spring Security вас пропустили
        return "me";
    }
}

Наразі нас цікавить не те, що повертає цей метод. Нас цікавить інше: чи взагалі доходить запит до контролера, чи Spring Security зупинить його раніше.

Припустімо, що ви вже бачили в логах під час запуску застосунку щось на кшталт “Using generated security password: …” і пам’ятаєте з минулої лекції, що типовий користувач називається user. Цього достатньо для тестів: анонімно — не пускає, з обліковими даними — пускає.

І тут важливо не бачити містики там, де її немає. Один і той самий захищений endpoint не змінює свої правила через примхи клієнта. Змінюється лише форма реакції на неавтентифікований запит: браузерний сценарій намагається довести людину до login page, а API-сценарій чесно відповідає 401. Тому порівнюємо ми не «три різні security», а один і той самий базовий сценарій очима різних клієнтів.

3. Браузер: редирект на login page

Коли ви відкриваєте URL у браузері, майже завжди це виглядає як навігація: ви або вводите адресу в рядку, або клікаєте посилання. У такому сценарії браузер очікує, що сервер поверне HTML-сторінку або хоча б покаже зрозумілий шлях «увійдіть до системи». Тому для захищених ресурсів типова «ввічлива» реакція — не просто сказати «401», а спрямувати користувача на login page.

Простими словами ланцюжок виглядає так: «Ти не автентифікований → отже, замість вмісту /api/me покажемо тобі місце, де можна увійти». І браузер автоматично слідує цьому сценарію, бо редиректи — звична частина вебнавігації.

Умовно це виглядає так (у вигляді маленької схеми):

sequenceDiagram
    participant B as Браузер
    participant S as "Сервер (Spring Boot + Security)"

    B->>S: GET /api/me
    S-->>B: 302 Found + Location: /login
    B->>S: GET /login
    S-->>B: "200 OK (HTML-сторінка входу)"

Ключовий момент: очима користувача ви одразу бачите login page, але мережею першою відповіддю був 302.

Щоб відчути це на практиці, можна «симулювати браузер» через curl, явно попросивши HTML. Ми спеціально додамо заголовок Accept: text/html, щоб сервер зрозумів: клієнт очікує HTML-орієнтовану відповідь.

# Просимо HTML, щоб побачити «браузерний» сценарій (редирект на /login)
curl -i -H "Accept: text/html" http://localhost:8080/api/me

Приклад того, що ви зазвичай побачите у відповіді (спрощено):

HTTP/1.1 302 Found
Location: http://localhost:8080/login
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache

Тепер, якщо попросити curl слідувати за редиректом опцією -L, ви отримаєте вже саму login page:

# Слідуємо за редиректом і отримуємо вже кінцеву відповідь сторінки входу
curl -i -L -H "Accept: text/html" http://localhost:8080/api/me

І там ви побачите щось на кшталт:

HTTP/1.1 200 OK
Content-Type: text/html;charset=UTF-8
...
<html>...login page...</html>

Саме тому новачкам здається, що /api/me «повертає HTML». Насправді /api/me просто закритий, а вас перенаправили на /login, бо клієнт виглядає браузерним.

Важлива методична думка: на цьому етапі login page — це не ваша фіча й не частина вашого API. Це лише стартова обгортка механізму, щоб ви могли побачити security в дії. У наступних блоках курсу ми будемо робити відповіді більш REST-friendly, але сьогодні наше завдання — навчитися не плутати редирект із фразою «зламалося».

4. Postman: 401 і редиректи

Postman здається «просто API-клієнтом», але на практиці вміє поводитися по-різному — і саме цим іноді підставляє новачка. Один і той самий запит ви можете побачити або як 401 Unauthorized, або як «раптом HTML-сторінка входу», і обидва варіанти можуть бути коректними залежно від налаштувань і заголовків.

Перший частий сценарій такий: ви робите запит GET /api/me, Postman отримує 302 Found і автоматично слідує за редиректом. У підсумку ви бачите 200 OK і HTML-контент /login. Створюється відчуття: «Чому замість JSON прийшов HTML?». Насправді Postman просто пішов маршрутом, який сервер запропонував браузеру.

Щоб побачити «сиру правду», корисно на якийсь час вимкнути автоматичне слідування за редиректами. У Postman це робиться в налаштуваннях, зазвичай через щось на кшталт Settings → General → Automatically follow redirects. Після цього той самий запит почне показувати 302 і заголовок Location, і картина стане чесною: це не «відповідь API», а «перенаправлення на login».

Другий сценарій ще корисніший саме для backend-розробника: ви явно говорите Postman, що хочете працювати як з API. Для цього можна додати заголовок:

Accept: application/json

Після цього багато security-конфігурацій, включно з типовою поведінкою за замовчуванням, починають відповідати не редиректом, а більш API-орієнтовано: статусом 401 Unauthorized. За цим важливо спостерігати, бо наш проєкт — саме API, і далі ми прагнутимемо до передбачуваних JSON-помилок.

І нарешті — найпрактичніше. Postman зручний тим, що в ньому легко надіслати облікові дані. У нашому випадку це означає просту річ: використовуємо тимчасового користувача за замовчуванням, якого Spring Boot дав нам для першого входу. Зазвичай ви робите так: у Postman переходите на вкладку Authorization, вибираєте тип “Basic Auth” і вводите:

Імʼя користувача: user
Пароль: той самий згенерований пароль із журналу запуску

Після цього запит на /api/me почне доходити до контролера і поверне 200 OK та тіло:

me

І тут важливо не захопитися й не почати думати: «О, отже, ми вже налаштували користувачів». Ні. Ми просто використовуємо тимчасовий технічний обліковий запис, щоб вручну перевірити: захищена зона справді стає доступною після автентифікації.

5. curl: статуси, заголовки, редиректи

curl — це як ліхтарик у темній кімнаті. Він не вдає, що все нормально, не прикрашає відповіді й не намагається вгадати, що ви мали на увазі. Якщо сервер сказав 302, ви побачите 302. Якщо сервер сказав 401, ви побачите 401. Для розуміння поведінки security на старті курсу це майже ідеальний інструмент: мінімум інтерфейсу, максимум фактів.

Одразу невелика таблиця — корисно тримати її поруч, щоб не гуглити щоразу:

Опція curl Що робить Навіщо нам сьогодні
-i
Показує заголовки відповіді + тіло Щоб бачити статус, Location, WWW-Authenticate
-L
Слідує за редиректами Щоб «дійти до login page» і зрозуміти, звідки вона береться
-u user:pass
Додає Basic credentials Щоб перевірити, що endpoint стає доступним
-H "Accept: ..."
Додає заголовок Accept Щоб «перемикати маску клієнта» (HTML vs JSON)

Тепер зберемо кілька спостережень навколо нашого /api/me.

API-запит: просимо JSON і отримуємо 401

Почнемо з варіанта з плану дня — запиту, який виглядає як запит API-клієнта:

# API-режим: просимо JSON, очікуємо 401 замість редиректу
curl -i -H "Accept: application/json" http://localhost:8080/api/me

Типово ви побачите щось на кшталт:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Realm"
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache

Сенс цієї відповіді людською мовою такий: «Я тебе не знаю, а ресурс закритий». На цьому етапі нам важливо не те, який саме механізм підказує сервер, а те, що замість редиректу ми побачили чесний HTTP-статус.

HTML-запит: редирект на /login

Тепер перемикаємо «маску клієнта»:

# Браузерний режим: просимо HTML і отримуємо редирект на /login
curl -i -H "Accept: text/html" http://localhost:8080/api/me

І бачимо:

HTTP/1.1 302 Found
Location: http://localhost:8080/login

Тобто буквально: «Якщо ти очікуєш HTML, я поведу тебе туди, де можна увійти».

Login page: переконуємося, що це HTML

Слідуємо за редиректом:

# Доходимо до кінцевої сторінки входу, щоб побачити, що це саме HTML
curl -i -L -H "Accept: text/html" http://localhost:8080/api/me

І переконуємося, що контент — HTML. Це корисно ще й тому, що новачки іноді починають шукати «контролер /login». А в нашому коді його може не бути: login page — частина стандартної поведінки.

Після автентифікації /api/me доступний

Тепер додамо облікові дані. Використовуємо default user user і generated password із логів — підставте свій пароль:

# Підставте свій пароль із журналу запуску: після цього очікуємо 200 OK і тіло "me"
curl -i -u user:6c5f7b8b-1d2e-4f40-9d63-1c6d0f8a1234 \
  -H "Accept: application/json" \
  http://localhost:8080/api/me

І очікуваний результат:

HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
...

me

Ось це і є «контрольна лампочка»: endpoint не зламаний, контролер працює, просто без автентифікації вас не пускали всередину.

І тут важливо втриматися від бажання все одразу «допиляти красиво». Сьогодні ми не робимо JSON-відповіді помилок, не будуємо DTO й не думаємо про ролі. Ми лише вчимося читати, що відбувається, коли security вже стоїть на вході.

6. Підсумки: один endpoint, три клієнти

Після всіх цих експериментів корисно спокійно зафіксувати результат, без містики. Spring Security не «дозволяє одному й тому самому URL бути різним». Він просто обирає такий стиль відповіді, який очікує клієнт. Для браузера — звичний вебсценарій з редиректом на login page. Для API-клієнта — статус 401, щоб клієнт міг програмно зрозуміти: потрібна автентифікація.

Ось невелика таблиця-нагадування для /api/me у стані «ми не увійшли»:

Клієнт Як виглядає запит Що зазвичай побачите
Браузер навігація, очікування HTML редирект на /login, потім HTML login page
Postman залежить від налаштувань і заголовків або 302/HTML (якщо follow redirects), або 401
curl ви керуєте заголовками вручну 401 при API-запиті, 302 при «HTML-запиті»

І головний висновок, який дуже стане в пригоді далі: не можна судити про поведінку захищеного API лише за браузером. Браузер показує вам сайт очима людини. А backend-розробнику треба ще вміти дивитися на API очима програми. Тому Postman і curl — не «другорядні іграшки», а нормальні інструменти для читання істини.

Тримайте цю різницю в голові щоразу, коли публічна, особиста або адміністраторська зона раптом «поводиться дивно». Дуже часто дивність зникає в ту саму секунду, коли ви розумієте, звідки саме взялися 302 і 401.

7. Типові помилки під час порівняння клієнтів

Помилка №1: «Відкрив у браузері, бачу login — отже, endpoint повертає HTML».
Це дуже часта пастка. Ви дивитеся на кінцеву сторінку, а не на першу відповідь сервера. Насправді /api/me може взагалі не віддавати HTML — він просто захищений, і вас перенаправили. Лікується просто: дивіться статус і заголовки в DevTools або через curl -i, і все стає на свої місця.

Помилка №2: «302 Found — отже, endpoint зламаний або не знайдений».
Новачок часто знає лише 200 і 404. Побачивши 302, він сприймає це як «якась дивна помилка». Але 302 — не помилка, а «я прошу тебе перейти за іншою адресою». У security-контексті це зазвичай запрошення пройти на login page.

Помилка №3: «Postman показав мені 200 OK, отже, я отримав доступ».
Іноді Postman показує 200, бо пішов за редиректом і отримав login page. Тобто цей 200 стосується /login, а не /api/me. Якщо цього не тримати в голові, можна тижнями «лікувати» неіснуючу проблему. Допомагає або вимкнення follow redirects, або явна перевірка final URL, або curl -i для чистоти експерименту.

Помилка №4: «401 Unauthorized — отже, такого endpoint немає».
401 — це «endpoint є, але ти не автентифікований». Для порівняння: якщо endpoint справді відсутній, ви побачите 404. Звичка розрізняти 401 і 404 — базова навичка backend-розробника, особливо в security-контексті.

Помилка №5: «Я не бачу generated password, отже, security не працює».
Generated password друкується під час запуску й змінюється після перезапуску, якщо ви не зафіксували користувача інакше. Якщо ви перезапустили застосунок і не зберегли пароль, то могли просто втратити його в логах. Це не «зламалося», це лише «ви не зберегли тимчасовий ключ від тимчасових дверей». На навчальному етапі це нормально: просто перезапустіть застосунок і візьміть актуальне значення із журналу запуску.

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