HttpSession і JSESSIONID

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

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
JSESSIONID
у браузері ідентифікатор «номерок із гардероба»
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 / StorageCookies → видаліть 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 коректно змінюється, наприклад після входу. Якщо ви це побачите — не лякайтеся і не думайте, що сервер зламався. Скоро ми розберемо, чому така поведінка не лише можлива, а й корисна з погляду захисту.

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