1. Транспорт і заголовки: реальність продакшена
Якщо до цього моменту Spring Security здавався вам чимось на кшталт «розумного турнікету на вході до контролерів», то сьогодні ми додамо ще один шар реальності: турнікет може бути ідеальним, але двері поруч усе одно мають зачинятися як слід. Будь-яка автентифікація та авторизація в нашому проєкті так чи інакше працює поверх HTTP: Authorization для Basic, cookie для сесії, Authorization: Bearer ... для JWT. Отже, транспорт і відповідні заголовки — це не декоративні прикраси, а умови, за яких ваша модель безпеки взагалі має сенс.
Уявіть, що ви зробили все правильно: ролі USER/EDITOR/ADMIN, @PreAuthorize, owner checks, JSON-помилки. У тестах усе зелене. І тут проєкт розгортають за reverse proxy, який термінує TLS. Додаток раптом «думає», що запити приходять по звичайному HTTP. У результаті можуть спливти дивні речі: десь редирект іде на http://..., cookie раптом не стає Secure, HSTS не працює, а деякі клієнти починають поводитися так, ніби ви «зламали вхід». І так — виглядати це буде так, ніби Spring Security «вередує», хоча винен контекст запиту.
У цій лекції ми триматимемо в голові просту думку: Spring Security захищає додатки в умовах HTTP-реальності, а HTTP-реальність у проді майже завжди включає HTTPS і reverse proxy. Тому нам потрібні три базові речі, які дають експлуатаційний baseline: що таке security headers (і чому їх не можна «вимикати пачкою»), навіщо HTTPS навіть для API, і як Spring Boot/Spring Security дізнаються, що «ззовні було HTTPS», якщо всередину до додатка дійшов HTTP.
Навіть якщо секрети й TTL уже винесені з коду, безпека все одно залежить від того, як запит реально дістається до додатка. Тому далі дивимося на зовнішній HTTP-контур: які заголовки ми віддаємо, коли додаток вважає запит secure і що змінюється, коли між клієнтом і додатком зʼявляється proxy.
2. Security headers: базовий режим Spring Security
Коли ви вперше бачите в конфігу http.headers(...), рука новачка іноді тягнеться до радикального рішення: «а давайте вимкнемо, раптом заважає». Це приблизно як лагодити клямку дверей, знімаючи двері з петель. Працює, але потім дивуєтеся протягам. Security headers — це набір HTTP response headers, які підказують клієнту, особливо браузеру, як поводитися безпечніше: що кешувати, чи можна вбудовувати сторінку в iframe, чи можна «вгадувати» тип контенту тощо.
У Spring Security тут підхід дуже дорослий: безпечна поведінка вмикається за замовчуванням. І це не «щоб ускладнити життя», а щоб ваш проєкт не залежав від дисципліни «ми потім обовʼязково ввімкнемо». У більшості курсів із security (і в реальному житті) біда приходить не від того, що хтось створив складну вразливість, а від того, що хтось залишив дірку «за замовчуванням». Заголовки — саме така історія: ви їх рідко помічаєте, доки не стається проблема.
У нашому проєкті Secure Content Platform API ці заголовки особливо важливі в тих місцях, де клієнтом потенційно є браузер. Навіть якщо ми зараз тестуємо через Postman, реальний світ часто включає Swagger UI, можливу інтеграцію зі SPA, адміністративні інструменти, сторінку логіну в stateful-гілці. Тому базовий принцип такий: спочатку зберігаємо налаштування за замовчуванням, а точкові зміни робимо лише тоді, коли розуміємо причину.
Мінімальний, правильний baseline прямо в SecurityFilterChain виглядає так:
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 SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Залишаємо заголовки безпеки Spring Security увімкненими за замовчуванням.
// Це безпечна базова конфігурація, яку потім можна точково налаштовувати.
http.headers(Customizer.withDefaults());
// Важливо: повертаємо зібраний ланцюжок фільтрів, інакше конфігурація не застосовується.
return http.build();
}
}
Сенс цього фрагмента не в тому, що «ми щось увімкнули». Сенс у тому, що ми явно фіксуємо: заголовки — частина моделі безпеки, і ми свідомо залишаємо безпечний baseline увімкненим.
3. Ключові security headers: практичний огляд
У заголовках легко потонути, бо їх багато, і кожен звучить як заклинання зі стародавнього манускрипту: X-Content-Type-Options, Strict-Transport-Security, Content-Security-Policy. Але нам, як backend-розробникам, потрібен практичний рівень: розуміти, які заголовки про що, чому Spring Security їх додає і чому їх масове вимкнення майже завжди погіршує ситуацію, навіть якщо «нічого не зламалося прямо зараз».
Важливо також памʼятати, що заголовки бувають різними за призначенням. Частина захищає від конкретних браузерних атак, наприклад clickjacking, частина — від «дивної поведінки» клієнта, наприклад sniffing типу контенту, частина — дисциплінує кешування. API-клієнти на кшталт curl у більшості випадків реагують на це нейтрально, але браузер — ні. А браузер у світі Java backend трапляється частіше, ніж здається: навіть якщо у вас «чистий REST», Swagger UI або проста адмінка вже перетворюють ваш сервіс на вебзастосунок.
Нижче — компактна таблиця для орієнтування, не як список «вивчити», а як карта місцевості:
| Заголовок | Ідея простими словами | До чого може призвести відсутність |
|---|---|---|
| X-Content-Type-Options: nosniff | «Не вгадуй тип контенту сам, довіряй Content-Type» | Браузер може спробувати інтерпретувати відповідь не так, як слід, і це іноді перетворюється на проблеми, схожі на XSS |
| X-Frame-Options або CSP frame-ancestors | «Не вбудовуй сторінку в чужий iframe» | Clickjacking: користувач клікає «по кнопці», не розуміючи, що вона підставна |
| Cache-Control, Pragma, Expires | «Не кешуй чутливі відповіді» | Приватні дані можуть опинитися в кеші браузера або проксі «про запас» |
| Strict-Transport-Security (HSTS) | «Якщо ти браузер, не ходи сюди по HTTP взагалі» | Користувач може випадково потрапити на HTTP-версію і наразитися на downgrade-атаку; HSTS знижує ризик |
| Content-Security-Policy (CSP) | «Які скрипти, стилі й ресурси взагалі дозволені» | Зменшує наслідки XSS, але потребує обережності; частіше важливий для сторінок з інтерфейсом, ніж для JSON |
Зверніть увагу на слово «браузер» у багатьох описах. Це нормально. Backend-розробнику важливо не плутати: CORS — це теж браузерна історія, CSRF — теж. Security headers потрапляють саме в цю категорію: «клієнтський» шар безпеки, але керує ним сервер.
І ще одна практична думка: заголовки самі по собі не замінюють @PreAuthorize, ролі, authorities та owner checks. Вони не вирішують задачу «хто може публікувати статтю», але допомагають зробити так, щоб клієнт не став вашим непередбачуваним співучасником атаки.
4. HTTPS для JSON API: що він дає
Про HTTPS новачок часто думає так: «ну в нас же токени й паролі не в URL, отже все гаразд». Це небезпечне спрощення. HTTPS (TLS) — це не про те, де зберігається токен, а про те, чи може хтось по дорозі прочитати або підмінити ваш HTTP-трафік. Якщо ви передаєте Authorization: Basic ..., то без HTTPS ви буквально надсилаєте логін і пароль у форматі, який легко відновити. Якщо ви передаєте cookie з JSESSIONID, то без HTTPS ви віддаєте ідентифікатор сесії. Якщо ви передаєте Authorization: Bearer ..., то без HTTPS ви віддаєте «ключ від квартири» у відкритому вигляді.
Навіть у випадку JWT, де «всередині підпис і все таке», bearer token залишається bearer: хто тримає токен, той і користувач. Жоден підпис не врятує, якщо токен витягнули з мережі. Тому транспортна безпека — це фундамент для всіх трьох моделей, які ми порівнювали в курсі: session, basic, JWT.
У додатку можна додати додатковий захист: явно сказати, що ми очікуємо тільки HTTPS. Для навчального baseline це корисно хоча б як страховка від випадкового запуску в неправильному оточенні. У Spring Security це виглядає так:
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
// Вимагаємо HTTPS для всіх запитів.
// Важливо: за reverse proxy це працюватиме коректно лише за налаштованих forwarded headers.
http.requiresChannel(channel -> channel
.anyRequest().requiresSecure()
);
Сенс цього шматка коду простий: якщо запит прийшов по HTTP, додаток намагатиметься перевести його на HTTPS, зазвичай через redirect. І тут зʼявляється важливе «але», через яке цей фрагмент у реальних проєктах завжди розглядають разом із reverse proxy: якщо TLS закінчується на проксі, а до додатка доходить HTTP, то для додатка всі запити виглядають як HTTP, і ви можете випадково отримати вічний редирект, дивну поведінку або ситуацію «в нас усе стало недоступним».
Тому правило таке: requiresSecure() — хороший інструмент, але він працює коректно лише тоді, коли додаток правильно розуміє, що зовнішній запит був HTTPS. І саме тут ми підходимо до reverse proxy та forwarded headers.
5. Reverse proxy у проді: роль і ефекти
Якщо ви досі запускали додаток як «прямо на порту 8080» і ходили до нього напряму, то прод зазвичай виглядає інакше. Перед вашим Spring Boot додатком стоїть reverse proxy: Nginx, Envoy, HAProxy, ingress-контролер у Kubernetes або навіть managed балансувальник у хмарі. Він приймає зовнішні запити, а вже потім передає їх усередину, до вашого додатка.
На людському рівні reverse proxy робить кілька речей одразу. Він часто закінчує TLS (тобто клієнт підключається по HTTPS до проксі, а проксі ходить до додатка по HTTP усередині мережі), уміє балансувати навантаження між кількома екземплярами сервісу, може стискати відповіді, обмежувати доступ за IP, виставляти rate limits. І так — іноді він «підкручує» заголовки так, що ваш додаток починає бачити інший контекст запиту.
Ось типова схема, на якій зазвичай і починається «HTTPS awareness»:
flowchart LR
%% Клієнт завжди взаємодіє із зовнішнім контуром (проксі), а додаток бачить уже «внутрішній» запит.
Client["Клієнт (браузер/SPA/Postman)"]
Proxy["Reverse proxy / LB
Термінація TLS"]
App["Додаток Spring Boot
Secure Content Platform API"]
Client -- "HTTPS" --> Proxy
Proxy -- "HTTP усередині мережі + Forwarded headers" --> App
Для вашого додатка тут ключовий момент: він бачить вхідний запит як HTTP. Але зовні клієнт спілкувався по HTTPS. Якщо додаток не вміє відновити «зовнішню реальність», зʼявляються ефектні баги: редиректи на http://, неправильні посилання у згенерованих відповідях, неможливість правильно виставити Secure cookie, HSTS не вмикається, а requiresSecure() починає зациклюватися.
І ні, це не «чисто DevOps». Це та точка, де backend-розробник має хоча б розуміти терміни, інакше ви будете лагодити симптоми в коді контролерів замість налаштування контексту запиту.
6. Forwarded headers: HTTPS awareness
Найчастіша причина «дивної поведінки за проксі» — додаток не розуміє первинну схему запиту. Для цього reverse proxy додає спеціальні заголовки. У навчальній реальності ви частіше зустрінете X-Forwarded-Proto: https, X-Forwarded-Host, X-Forwarded-Port, а іноді стандартний Forwarded: proto=https;host=... Ці заголовки несуть одну ідею: «зовнішній запит був таким-то, просто я, проксі, дійшов до тебе іншим шляхом».
Spring Boot уміє враховувати ці заголовки, але це потрібно увімкнути і важливо увімкнути правильно. У межах нашого baseline найпростіший і зрозумілий варіант — налаштувати стратегію обробки forwarded headers на рівні сервера додатка:
server:
# Кажемо Spring, що потрібно враховувати заголовки Forwarded / X-Forwarded-* від reverse proxy.
# Це потрібно, щоб коректно відновлювалися scheme/host/port (наприклад, HTTPS «ззовні»).
forward-headers-strategy: framework
Це налаштування каже приблизно таке: «використовуй можливості Spring Framework, щоб обробити forwarded headers і правильно відновити схему, хост і порт запиту». Після цього багато речей починають працювати очікувано: request.isSecure() стає true там, де зовнішній запит дійсно був HTTPS, правильніше формуються редиректи, коректніше працюють рішення, які привʼязані до схеми.
Тут є важлива security-обмовка, яку варто проговорити саме на рівні основ. Forwarded headers — це заголовки, а заголовки може підробити клієнт. Якщо ваш додаток бездумно довіряє forwarded headers від будь-кого, зловмисник може спробувати зіграти роль проксі і підсунути X-Forwarded-Proto: https, щоб додаток вважав запит secure. Тому в реальних системах довіру до forwarded headers зазвичай обмежують на рівні інфраструктури: проксі має сам перезаписувати такі заголовки, а прямий доступ до додатка ззовні — бути закритий. Ми не йдемо в DevOps-налаштування, але ви маєте розуміти, чому це важливо: «HTTPS awareness» — це не віра в заголовки, а коректно вибудуваний ланцюжок «клієнт → проксі → додаток».
Якщо після ввімкнення forwarded headers ви використовуєте requiresSecure(), то логіка стає адекватною: додаток бачить secure-запит і не намагається безкінечно редиректити сам себе. Плюс ви отримуєте більш коректну поведінку заголовків, привʼязаних до HTTPS, наприклад HSTS (він має сенс лише тоді, коли браузер уже спілкується з вами по HTTPS).
7. Типові помилки під час роботи з security headers, HTTPS і reverse proxy
У цій темі помилки особливо підступні тим, що виглядають як «Spring Security знову чудить», хоча насправді це просто невідповідність контексту запиту й ваших очікувань. І чим ближче ви до продакшена, тим частіше такі помилки зʼявляються «раптово», бо локально ви працювали без проксі та без HTTPS.
Помилка №1: глобальне headers().disable() «щоб не заважало».
Це класика. Заголовки рідко заважають вашому API-клієнту, але часто захищають браузерного клієнта від неприємних сценаріїв. Вимикаючи їх, ви зазвичай не вирішуєте проблему, а робите проєкт менш захищеним і ускладнюєте майбутній аудит. Якщо щось «заважає», краще точково налаштувати конкретний header, а не вимикати все.
Помилка №2: думати, що «в нас JWT, отже HTTPS не обовʼязковий».
JWT — не бронежилет, а формат токена. Якщо bearer token пішов по HTTP, він пішов у відкритому вигляді. Це той випадок, коли криптографія всередині токена не рятує від витоку по дорозі. HTTPS потрібен і session, і basic, і JWT — просто з різних причин, але підсумок один: без транспорту вся модель тріщить.
Помилка №3: увімкнути requiresSecure(), але забути про reverse proxy та forwarded headers.
На localhost це може виглядати «нормально», а в проді за проксі починається цикл редиректів або повна відмова в доступі. Причина майже завжди одна: додаток бачить вхідний запит як HTTP і чесно намагається «перевести його на HTTPS», не розуміючи, що HTTPS уже був зовні.
Помилка №4: довіряти forwarded headers від будь-кого.
Якщо додаток доступний напряму з інтернету, клієнт може підробити X-Forwarded-Proto та інші заголовки. Тому «HTTPS awareness» — це не «вірити заголовкам», а «бути за проксі, який ці заголовки контролює». На рівні курсу достатньо запамʼятати: forwarded headers — річ корисна, але довіра до них має бути усвідомленою.
Помилка №5: намагатися лагодити транспортні проблеми в контролерах.
Іноді бачиш код, де люди вручну переписують посилання, додають дивні Location заголовки або «приклеюють https://» рядком. Це зазвичай ознака того, що додаток не розуміє первинну схему запиту. Такі речі мають вирішуватися на рівні налаштування forwarded headers і security-конфігурації, а не всередині бізнес-логіки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ