1. HTTP не пам’ятає минулі запити
Якщо ви тільки починаєте працювати з бекендом, дуже легко уявити HTTP як телефонну розмову: «я ж сказав серверу, хто я, то чому він не пам’ятає?». Але HTTP набагато більше схожий на спілкування із золотою рибкою: кожен запит живе окремо, і за замовчуванням сервер не зобов’язаний пам’ятати, що було секунду тому. Саме це й називають stateless-природою HTTP.
Уявіть наш проєкт Secure Content Platform API. Ви зайшли через formLogin і потім робите GET /api/me. Якби кожен запит існував у вакуумі, сервер щоразу мав би заново запитувати: «А ви хто?». У браузері це виглядало б як нескінченне «введіть логін і пароль» перед кожним кліком. Для користувача це було б суцільним катуванням, а для бізнесу — приводом його втратити.
У реальних застосунках нам потрібно зв’язати запити в один ланцюжок: «це все ще той самий користувач, який щойно успішно ввійшов». І тут з’являється ключова ідея дня: серверна сесія.
Якщо сказати зовсім коротко, HTTP сам по собі не зберігає минулого. Тому нам потрібен окремий механізм, який зв’язуватиме один запит з іншим і не ламатиме саму модель HTTP. У servlet-світі цим механізмом і стає сесія.
2. Сесія: що це і де живе
Сесія — це спосіб зробити спілкування із сервером схожим на розмову з пам’яттю, не порушуючи HTTP. Сам HTTP залишається stateless, а ми додаємо домовленість: сервер створює шматочок стану, а клієнт носить із собою «номерок», за яким сервер щоразу знаходить цей шматочок. У Java servlet-світі цей шматочок стану представлений інтерфейсом HttpSession.
Важливо одразу прибрати найпоширенішу ілюзію новачка: сесія — не в браузері. Браузер зберігає лише маленький ідентифікатор, зазвичай cookie, а вся «пам’ять» лежить на сервері — у пам’яті застосунку або в іншому сховищі, яке використовує сервер. У межах нашого курсу тримаємо це просто: «на сервері».
Можна тримати в голові таку таблицю — вона швидко розрулює плутанину між «сесією» та «куками».
| Сутність | Де живе | Що зберігає | Розмір/сенс |
|---|---|---|---|
| HttpSession | на сервері | стан (атрибути) | «пам’ять розмови» |
cookie |
у браузері | ідентифікатор | «номерок із гардероба» |
| HTTP запит | летить мережею | усе, що ви відправили (URL, headers, body) | один кадр, без пам’яті |
І ще одна важлива думка, щоб мозок перестав малювати «супероб’єкт користувача» всюди: HttpSession — це найчастіше просто сховище ключ-значення, у яке сервер може покласти кілька атрибутів. Це не база даних. Не кеш на 2 гігабайти. Не місце для «давайте покладемо туди весь UserAccount разом з аватаркою та чернетками».
3. Cookie-ідентифікатор JSESSIONID
Якщо HttpSession живе на сервері, то як клієнт узагалі пов’язаний із нею? Потрібен механізм, який дозволяє клієнту сказати: «сервере, знайди мою сесію». У servlet-застосунках це зазвичай робиться через cookie з іменем JSESSIONID.
Cookie — це маленький фрагмент даних, який браузер зберігає у себе й автоматично прикладає до запитів на той самий сайт, точніше — згідно з правилами домену, шляху та атрибутів cookie. Сервер у відповіді може сказати: «браузере, збережи ось цю cookie», і браузер слухняно збереже.
Шлях виглядає приблизно так — це важливіше запам’ятати, ніж будь-який DSL.
sequenceDiagram
participant B as Браузер
participant S as "Сервер (Spring Boot)"
B->>S: "GET /api/me, уперше, без cookie"
S->>B: "302/200 + Set-Cookie: JSESSIONID=abc123..."
B->>S: "GET /api/me, удруге, з Cookie: JSESSIONID=abc123..."
S->>B: "200 OK, сервер упізнав клієнта за id"
Покажімо ще простіше — але протокольно — різницю до і після.
Запит №1 — ще немає cookie:
GET /session/id HTTP/1.1
Host: localhost:8080
Відповідь №1 — сервер створює сесію і каже браузеру зберегти ідентифікатор:
HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=E1A3...; Path=/; HttpOnly
Запит №2 — браузер уже «пам’ятає номерок» і прикладає cookie автоматично:
GET /session/id HTTP/1.1
Host: localhost:8080
Cookie: JSESSIONID=E1A3...
Зараз принципово важливо зрозуміти вміст цієї cookie: всередині не лежить пароль, не лежить роль, не лежить об’єкт користувача. Там лежить ідентифікатор, який сам по собі нічого не говорить про користувача. Він лише дозволяє серверу сказати: «ага, це той клієнт, у якого сесія з id E1A3...».
Це як номерок у гардеробі. Номерок не є вашою курткою. Він навіть не доводить, що куртка ваша, але він дозволяє працівнику гардероба знайти потрібну вішалку.
4. HttpSession у контролері Spring MVC
Зараз буде найприємніша частина: у Spring MVC усе можна помацати руками буквально в кілька рядків. Це корисно методично: ви перестаєте вірити на слово і починаєте спостерігати реальну поведінку застосунку. А спостережувана поведінка — найкращий антидот проти магії.
Приклад 1: ендпоінт, який повертає ID сесії
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class SessionInfoController {
@GetMapping("/debug/session/id")
String sessionId(HttpSession session) {
// Важливо: сам факт наявності HttpSession у параметрах зазвичай означає,
// що за відсутності сесії сервер її створить.
return session.getId(); // Повертаємо поточний ідентифікатор серверної сесії
}
}
Що тут відбувається на практиці:
Spring бачить параметр HttpSession і підставляє вам поточну сесію. Якщо сесії ще не було, сервер створить її в більшості типових конфігурацій. У відповідь браузер отримає Set-Cookie: JSESSIONID=..., і далі браузер автоматично прикладатиме cookie.
Якщо ви кілька разів оновите сторінку або кілька разів викличете ендпоінт через браузер, то, доки cookie не очищені і сесія жива, ви бачитимете один і той самий session.getId().
Важливий нюанс для уважних: ін’єкція HttpSession — це зручний спосіб «підхопити» сесію. Але в цього є логічний наслідок: ви ніби кажете застосунку «сесія мені потрібна». Тобто навіть ваш суто інформаційний ендпоінт може ініціювати створення сесії, якщо її не було.
Іноді в суворіших сценаріях хочеться так: «якщо сесії немає — не створювати». Тоді беруть HttpServletRequest і викликають getSession(false). Це виглядає так:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class OptionalSessionController {
@GetMapping("/debug/session/maybe")
String maybeSession(HttpServletRequest request) {
// false = "не створюй нову сесію, якщо її ще немає"
HttpSession session = request.getSession(false);
// Тут явно обробляємо ситуацію "сесії немає"
return session == null ? "немає сесії" : session.getId();
}
}
Ми не робимо з цього великий блок, просто фіксуємо думку: сесія може з’являтися не сама, а тому що ви її попросили.
5. Атрибути сесії: стан на сервері
Після того як ми навчилися отримувати sessionId, хочеться зробити наступний крок: переконатися, що сесія — це справді пам’ять, а не просто випадковий рядок у cookie. Для цього ідеально підходить лічильник візитів — він дуже простий, але показує рівно те, що потрібно: значення переживає кілька запитів.
Приклад 2: лічильник відвідувань у HttpSession
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class VisitController {
@GetMapping("/debug/session/visits")
Integer visits(HttpSession session) {
// Дістаємо значення із сесії (на сервері). getAttribute повертає Object.
Integer visits = (Integer) session.getAttribute("visits");
// Якщо атрибута немає — це перший візит у межах цієї сесії.
int next = (visits == null) ? 1 : (visits + 1);
// Кладемо оновлене значення назад у серверну сесію.
session.setAttribute("visits", next);
// Повертаємо лічильник клієнту лише для спостереження за поведінкою.
return next;
}
}
Що важливо помітити:
getAttribute повертає Object, тому ми приводимо тип до Integer. Якщо атрибута ще немає, ми створюємо його і кладемо в сесію. На наступному запиті ми дістаємо значення знову — воно зберігається на сервері, а не в браузері.
Якщо ви відкриєте цей ендпоінт в режимі інкогніто, то побачите окремий лічильник. Чому? Тому що в інкогніто окреме сховище cookie, а отже й інший JSESSIONID, а отже й інша HttpSession на сервері.
Цей приклад не закликає зберігати все на світі в сесії. Він показує механічну основу: є серверне сховище і є ідентифікатор, який веде до нього. У нашому security-контексті ми використовуватимемо це саме як фундамент для стійкого автентифікованого стану.
6. Спостерігаємо cookie в браузері
Сесію корисно розуміти не лише як Java-інтерфейс, а й як видиму поведінку клієнта. Це як вчитися керувати автомобілем: можна читати правила, а можна один раз сісти й відчути педалі. Ми зараз і відчуємо педалі через DevTools, бо там усе чесно: які headers прийшли, які cookie встановилися, що відправилося назад.
Відкрийте в браузері будь-яку кінцеву точку /debug/session/id. Далі зайдіть у DevTools і знайдіть вкладку Network. Зробіть запит ще раз і подивіться на заголовки відповіді: там має бути Set-Cookie, де фігурує JSESSIONID.
Потім зверніть увагу на наступний запит: у його заголовках, у заголовках запиту, буде Cookie: JSESSIONID=.... Це і є магічна ниточка, яка зв’язує два запити в одну розмову.
Додаткова вправа для мозку: видаліть cookie сайту через Application / Storage → Cookies → видаліть JSESSIONID і повторіть запит. Ви побачите, що session.getId() змінився, і знову з’явився Set-Cookie — сервер починає розмову з чистого аркуша.
7. Зв’язок зі Spring Security
Тепер у вас може виникнути запитання: «Гаразд, сесія є. А до чого тут безпека?». Чудове запитання — і саме заради нього ми так довго розбиралися з JSESSIONID. Сесія — це не фіча Spring Security, а інфраструктурна штука servlet-світу. А Spring Security просто дуже грамотно використовує її, щоб не змушувати користувача підтверджувати особу на кожному кроці браузерного сценарію.
Коли ми ввімкнули formLogin у попередній лекції, ми отримали класичний вебсценарій: користувач надсилає логін і пароль, Spring Security автентифікує його і далі зберігає результат. У session-based моделі цей результат зберігається в серверній сесії, а браузер носить JSESSIONID, який дозволяє на кожному наступному запиті відновити цей результат.
Важлива точка: браузер не надсилає пароль знову і знову. Він надсилає лише cookie. Сервер отримує cookie, знаходить сесію, дістає з неї збережений security-стан — і вже на цій основі вирішує, чи можна пускати запит на /api/me, /api/drafts і так далі.
Ми не занурюємося в деталі, які саме класи Spring Security це роблять і де зберігається SecurityContext — це наступна лекція. Зараз зафіксуйте головне: session-based безпека фізично тримається на парі «серверна сесія + cookie-ідентифікатор».
8. Типові помилки під час роботи з HttpSession
Помилка №1: думка «користувач зберігається в cookie».
Дуже хочеться уявити, що браузер десь носить об’єкт користувача. Але cookie JSESSIONID — це не користувач і не доказ особи, а лише ідентифікатор сесії. Реальні дані та стан зберігаються на сервері. Якщо ви почнете сприймати cookie як мінібазу користувача, потім дивуватиметеся, чому достатньо вкрасти cookie, щоб імітувати сесію.
Помилка №2: плутанина між HttpSession і cookie під спільним словом «сесія».
У розмовній мові так роблять навіть досвідчені розробники, але новачку це ламає розуміння. Якщо тримати термінологію суворо, то HttpSession — це серверна сутність, а cookie — клієнтський механізм доставки ідентифікатора. Коли ви розводите ці дві ролі, стає набагато простіше розуміти, що відбувається на рівні HTTP.
Помилка №3: випадкове створення сесій усюди.
Якщо ви всюди приймаєте HttpSession як параметр або в кожному запиті викликаєте request.getSession(), ви тим самим кажете серверу: «створи сесію навіть там, де вона не потрібна». У навчальних експериментах це допустимо, але важливо усвідомлювати ефект. Іноді правильніше перевірити getSession(false) і поводитися інакше, якщо сесії ще немає.
Помилка №4: зберігання в сесії занадто великого обсягу даних.
Сесія — це стан, але він має бути компактним і зрозумілим. Якщо ви почнете складати туди великі списки, DTO-об’єкти, результати запитів, шматки файлів і все, що «шкода перераховувати», застосунок швидко стане важким і непередбачуваним. У security-контексті сесія потрібна, щоб тримати «я автентифікований» і мінімальні пов’язані дані, а не щоб замінити нормальні шари застосунку.
Помилка №5: очікування, що ідентифікатор сесії ніколи не змінюється.
На перших кроках ви бачите стабільний session.getId(), і здається, що так буде завжди. Але в реальній безпеці можливі моменти, коли ID коректно змінюється, наприклад після входу. Якщо ви це побачите — не лякайтеся і не думайте, що сервер зламався. Скоро ми розберемо, чому така поведінка не лише можлива, а й корисна з погляду захисту.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ