1. Час — межі чинності JWT
Якби JWT не вмів «старіти», він був би дуже зручним… для зловмисника. Уявіть ключ від квартири, який видали один раз і забули: ви можете вже пʼять років не жити в цій квартирі, а ключ і досі відчиняє двері. У світі безпеки так не можна, тому токен майже завжди має часові межі. Вони розв’язують одразу дві практичні задачі: зменшують вікно ризику під час витоку токена і дають системі важіль керування — можна змінювати політику часу життя, не переписуючи весь механізм доступу.
Щойно в контракті токена зʼявляються iat, nbf і exp, питання стає дуже практичним: коли ця перепустка взагалі починає діяти, коли її ще зарано приймати, а коли вже запізно. Без цього JWT залишається лише рядком із числами, а не керованим вікном доступу.
У нашому проєкті Secure Content Platform API JWT відіграватиме роль «перепустки» для доступу до приватних зон (/api/me, робота з чернетками, editor/admin zones). Але навіть якщо токен підписано ідеально, він усе одно залишається bearer token: хто тримає токен у руках, той і проходить як ви. Тому часові обмеження — це спосіб сказати: «Так, це перепустка… але діє вона недовго».
Щоб це стало не гаслом, а інженерною картиною, зафіксуймо просту модель «вікна чинності» токена.
flowchart TD
A["iat: токен видано"] --> B["nbf: токен стає допустимим"]
B --> C["exp: токен перестає бути допустимим"]
В ідеальному простому сценарії nbf збігається з iat, тобто токен допустимий одразу після випуску. Але сам факт, що в JWT є нижня межа (nbf) і верхня межа (exp), робить його керованим об’єктом, а не вічною табличкою «користувач = Вася».
2. NumericDate: час у секундах
Коли новачок уперше бачить exp: 1773828900, він зазвичай підозрює два варіанти: або це «шифр», або розробники JWT — таємне товариство любителів калькуляторів. Насправді все простіше: стандарт JWT описує час у форматі NumericDate, а це, по суті, кількість секунд від початку епохи Unix (1970-01-01T00:00:00Z). Таке представлення зручне для машин, не залежить від часових поясів і не вимагає домовлятися, у якому форматі записувати дату рядком.
Ключова думка для backend-розробника: у JWT час майже завжди в секундах, а не в мілісекундах. І ось тут живе одна з найприкріших помилок: сервер записав мілісекунди, а валідатор думає, що це секунди — і токен раптом стає «дійсним до кінця часів» (ну, приблизно доти, доки не згасне Сонце, а воно ще тримається).
Подивімося на це руками в Java. Ми спеціально беремо фіксований час, щоб вивід був передбачуваним.
import java.time.Instant;
public class EpochDemo {
public static void main(String[] args) {
// NumericDate в JWT — це секунди від Unix epoch, а не мілісекунди
long expSeconds = 1773828900L;
// Перетворюємо epoch-seconds в Instant (UTC-момент часу)
Instant exp = Instant.ofEpochSecond(expSeconds);
// Для налагодження зручно друкувати Instant: видно час у UTC
System.out.println(exp); // 2026-03-17T??:??:??Z (залежить від числа)
}
}
Якщо хочеться побачити «людське» представлення ще приємніше, можна форматувати через ZonedDateTime, але для логіки безпеки зазвичай достатньо Instant: він завжди в UTC і не змушує вас думати про часові пояси там, де це не потрібно.
Важливо також розуміти, що NumericDate — це не «час сервера». Це просто абсолютна точка на часовій шкалі. Сервер може жити в Нью-Йорку, клієнт — у Токіо, а токен — в UTC. І це добре: токен не має залежати від того, де ви фізично перебуваєте.
3. iat, nbf, exp: вікно чинності токена
iat: момент випуску токена
iat (issued at) звучить дуже прямо: «видано тоді-то». Але в цій прямоті є важлива практична цінність. Поле iat допомагає відповідати на запитання, які ви неминуче почнете ставити під час налагодження: «коли цей токен узагалі зʼявився?», «чому він ще діє?», «чому в користувача токен “із майбутнього”?». Це поле також допомагає розрізняти «старі» токени і «нові» хоча б на рівні розуміння, навіть якщо ви не будуєте складну інфраструктуру керування токенами.
У навчальному проєкті iat корисне ще й як точка, від якої зручно обчислювати TTL: зазвичай TTL — це exp - iat. Ми не робимо з цього криптографічну магію — ми робимо з цього бухгалтерію часу.
Приклад: обчислюємо iat і exp для токена, який живе 15 хвилин. Тут важливо зафіксувати думку: значення в JWT — у секундах.
import java.time.Duration;
import java.time.Instant;
public class IatExpDemo {
public static void main(String[] args) {
// Фіксуємо момент випуску токена (iat) в UTC
Instant issuedAt = Instant.parse("2026-03-18T10:00:00Z");
// exp зазвичай рахують як iat + TTL
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(15));
// У JWT зберігаємо секунди (NumericDate), тому беремо epochSecond
System.out.println(issuedAt.getEpochSecond()); // 1773828000 (приклад)
System.out.println(expiresAt.getEpochSecond()); // 1773828900 (приклад)
}
}
Чому iat не гарантує, що токен уже працює? Тому що для цього є nbf. У нормальному сценарії вони збігаються, але стандарт залишає можливість сказати: «токен випущено зараз, але діяти він почне трохи пізніше». І це приводить нас до наступного поля.
nbf: момент початку дії токена
nbf (not before) — це нижня межа чинності. Логіка проста: навіть якщо токен ідеально підписано й він виглядає правильним, сервер не має приймати його раніше вказаного моменту. На практиці багато систем узагалі не використовують nbf і ставлять його рівним iat або не додають у токен. Це нормально: не потрібно ускладнювати контракт, якщо у вас немає задачі відкладеної чинності.
Але nbf корисно розуміти, тому що воно пояснює типовий сценарій «токен наче є, але сервер каже, що він ще невалідний». Новачки часто сприймають це як «щось зламалося», а іноді це просто коректна робота nbf.
Покажімо це на мініперевірці. Ми не перевіряємо підпис (це окрема історія), ми лише дивимося на часовий інтервал.
import java.time.Instant;
public class NbfDemo {
public static void main(String[] args) {
// "Поточний час" (наприклад, на сервері) — для прикладу фіксуємо його
Instant now = Instant.parse("2026-03-18T10:00:10Z");
// Нижня межа чинності токена: раніше цього моменту приймати не можна
Instant notBefore = Instant.parse("2026-03-18T10:00:30Z");
// usableNow = now >= notBefore
boolean usableNow = !now.isBefore(notBefore);
// Зараз токен ще "не набув чинності"
System.out.println(usableNow); // false
}
}
Якщо говорити людською мовою: токен існує, але ще не набув чинності.
Коли це може бути потрібно? Наприклад, у системах, де токен випускається завчасно, або під час міграцій між сервісами, або за деяких видів clock skew (про нього трохи пізніше). Але навіть якщо ви ніколи не будете навмисно використовувати nbf, ви маєте вміти читати його й не лякатися.
exp: момент «смерті» токена
exp (expiration time) — верхня межа чинності. Після цього моменту токен не має прийматися. Саме exp робить access token «тимчасовою перепусткою», а не «вічним паспортом». І це один із найважливіших антихаотичних механізмів у stateless-моделі: якщо ви не можете миттєво «забрати» вже виданий токен, то хоча б можете гарантувати, що він помре сам.
Тут важливо запам’ятати просте правило перевірки: якщо now >= exp, то токен прострочений. Не «приблизно», не «майже», не «ну гаразд, він лише на секунду запізнився» — формально він уже невалідний. Утім, реальний світ усе одно підкине вам clock skew, і ми це обговоримо далі.
Мініперевірка на Java:
import java.time.Instant;
public class ExpDemo {
public static void main(String[] args) {
// Фіксуємо "поточний час"
Instant now = Instant.parse("2026-03-18T10:15:00Z");
// Час смерті токена
Instant exp = Instant.parse("2026-03-18T10:15:00Z");
// Формальне правило: now >= exp => token expired
boolean expired = !now.isBefore(exp); // now >= exp
// На межі "рівно exp" токен уже вважається простроченим
System.out.println(expired); // true
}
}
Так, на межі «рівно exp» токен уже вважається простроченим. Це іноді дивує, але зате робить правило однозначним. Security взагалі любить однозначність: вона гірша для романтики, але краща для експлуатації.
Ще один важливий момент: exp — це час, а не «кількість хвилин». Тому TTL — це похідна величина: ми обчислюємо його, віднімаючи iat від exp. І ось тут ми переходимо до TTL як до інженерного й продуктового рішення.
4. TTL токена: компроміс зручності й ризику
TTL (time to live) — це «скільки часу живе токен». У JWT його зазвичай не зберігають окремим полем (хоча іноді додають як custom claim, що частіше надлишково). TTL майже завжди виражають через iat і exp: TTL = exp - iat. І саме TTL перетворює «абстрактну безпеку» на конкретний компроміс між зручністю й ризиком.
Якщо TTL дуже короткий, користувачу може бути незручно: токен часто закінчується, клієнту доводиться знову отримувати доступ. Якщо TTL дуже довгий, зручно всім… окрім вашої системи безпеки і чергового розробника, який буде розбиратися, чому «у нас вкрали токен учора, а він досі працює сьогодні».
Для навчального Secure Content Platform API розумно мислити TTL як «коротке, кероване вікно». Наприклад, 15 хвилин — гарний навчальний орієнтир: достатньо довго, щоб не дратувати, і достатньо коротко, щоб сенс обмеження був відчутний.
Порахуємо TTL прямо в коді, щоб не залишати це «на око».
import java.time.Duration;
import java.time.Instant;
public class TtlDemo {
public static void main(String[] args) {
// iat і exp задаємо явно, щоб бачити TTL без прив'язки до "зараз"
Instant iat = Instant.parse("2026-03-18T10:00:00Z");
Instant exp = Instant.parse("2026-03-18T10:15:00Z");
// TTL = exp - iat
Duration ttl = Duration.between(iat, exp);
// Зручно перевіряти очікування в хвилинах
System.out.println(ttl.toMinutes()); // 15
}
}
Коли ви обговорюєте TTL із командою або з собою з майбутнього, корисно говорити не «поставимо exp», а «який TTL нам потрібен і чому». Це робить рішення осмисленим: ви вибираєте вікно ризику, а не випадкове число.
І ще одна важлива практична річ: TTL майже завжди має бути конфігурованим, тому що «15 хвилин» у dev-середовищі і «15 хвилин» у prod можуть мати різну ціну. Але в межах цієї лекції ми фіксуємо лише ментальну модель: TTL — це частина контракту і частина безпеки, а не «параметр за замовчуванням».
І тут легко переоцінити силу exp. Короткий TTL зменшує вікно, упродовж якого вкрадений токен або застарілі claims залишаються небезпечними. Але TTL не переписує вміст уже виданого payload і не робить миттєвий відклик: він лише обмежує шкоду в часі.
5. clock skew: допуск на розходження годинників
Clock skew — це розходження годинників між системами. Навіть якщо ви дуже любите точність, ваші сервери і клієнти можуть жити в різних часових реальностях. Один сервер синхронізувався по NTP, інший — трохи відстав, на віртуалці час «поплив», контейнер стартував і перші секунди живе дивно, а десь ще клієнтський телефон вирішив, що зараз 2017-й, бо користувач так захотів. У підсумку «правильний» токен може виглядати «з майбутнього» або «вже простроченим», якщо перевіряти час без допуску.
Тому багато систем вводять допуск: наприклад, дозволяють невелике розходження на кшталт 10–30 секунд. Це не «послаблення безпеки заради лінощів», а інженерне страхування від хибних відмов. Головне — не перетворювати clock skew на «можна все»: допуск має бути невеликим, інакше ви справді розширюєте вікно чинності токена.
Покажімо ідею: токен формально «прострочений» на 5 секунд, але ми дозволяємо 10 секунд допуску.
import java.time.Duration;
import java.time.Instant;
public class ClockSkewDemo {
public static void main(String[] args) {
// Поточний час (у валідатора)
Instant now = Instant.parse("2026-03-18T10:15:05Z");
// exp прийшов із токена
Instant exp = Instant.parse("2026-03-18T10:15:00Z");
// Допуск на розходження годинників
Duration skew = Duration.ofSeconds(10);
// Строга перевірка: now >= exp => expired
boolean expiredStrict = !now.isBefore(exp);
// Перевірка з допуском: ніби "наші годинники трохи поспішають"
boolean expiredWithSkew = !now.minus(skew).isBefore(exp);
System.out.println(expiredStrict); // true
System.out.println(expiredWithSkew); // false
}
}
Зверніть увагу на думку: ми не «зсуваємо exp уперед», ми зсуваємо поточний час назад на невелику величину допуску. Це зручна ментальна модель: «давайте перевіримо, що буде, якщо наші годинники трохи поспішають».
Точно так само можна застосовувати skew до nbf: якщо токен «ще не настав», але лише на пару секунд, і ми допускаємо розходження, його можна прийняти.
6. Перевірка часу токена без криптографії
Дуже легко переплутати: «валідація JWT» — це одразу підпис, ключі, алгоритми, парсинг, обробка помилок. Але часову частину перевіряють окремо, простим шаром. І корисно вміти проговорити цей шар окремо, бо він потім допомагає налагоджувати: ви принаймні розумієте, токен «падає» через час чи через щось інше.
Ментально перевірка часу виглядає як «вікно допустимості»: токен має бути вже «не раніше ніж nbf» і ще «не пізніше ніж exp». Якщо nbf відсутній — вважаємо, що нижньої межі немає (або вона дорівнює iat, якщо ви так вирішили в контракті). Якщо iat відсутній — це не завжди критично для чинності, але зазвичай дивно для контракту. У навчальному проєкті ми дотримуватимемося простого, читабельного контракту, де iat і exp є завжди.
Нижче — маленький приклад «часового валідатора». Він не робить JWT безпечним (підпис ніхто не перевіряє), але він показує саме той шматок логіки, який стосується часу.
import java.time.Duration;
import java.time.Instant;
public class JwtTimeValidator {
public static boolean isTimeValid(Instant now, Instant nbf, Instant exp, Duration skew) {
// Для nbf допускаємо невеликий skew у бік "ми могли трохи відставати"
// Перевірка: now + skew >= nbf
boolean notTooEarly = !now.plus(skew).isBefore(nbf);
// Для exp допускаємо невеликий skew у бік "ми могли трохи поспішати"
// Перевірка: now - skew < exp
boolean notTooLate = now.minus(skew).isBefore(exp);
// Часова частина валідна лише тоді, коли токен уже активний і ще не закінчився
return notTooEarly && notTooLate;
}
public static void main(String[] args) {
// У прикладі фіксуємо час, щоб поведінка була передбачуваною
Instant now = Instant.parse("2026-03-18T10:00:10Z");
Instant nbf = Instant.parse("2026-03-18T10:00:00Z");
Instant exp = Instant.parse("2026-03-18T10:15:00Z");
// У реальному коді skew беруть із конфігурації
System.out.println(isTimeValid(now, nbf, exp, Duration.ofSeconds(10))); // true
}
}
Тут дві невеликі «магії» — насправді анти-магії. Для nbf ми перевіряємо «не надто рано» з урахуванням допуску: now + skew >= nbf. Для exp — «не надто пізно» з урахуванням допуску: now - skew < exp. Так ви допускаєте невелику похибку годинників в обидва боки й не отримуєте неочікуваних відмов на рівному місці.
Якщо хочеться закріпити картину візуально, ось той самий алгоритм як блок-схема.
flowchart TD
A["Є now, nbf, exp, skew"] --> B{"now + skew < nbf ?"}
B -- "так" --> E["Занадто рано: токен ще не активний"]
B -- "ні" --> C{"now - skew >= exp ?"}
C -- "так" --> F["Занадто пізно: токен прострочений"]
C -- "ні" --> D["Часова частина валідна"]
Це корисно, бо в логах і під час налагодження ви зможете пояснювати собі й колегам: «Ми відхилили токен, бо він ще не активний» або «бо він прострочений», замість загального «JWT невалідний».
7. Типові помилки під час роботи з часом JWT
Помилка № 1: переплутати секунди й мілісекунди.
Найчастіша і найтоксичніша помилка. JWT чекає NumericDate у секундах, а розробник за звичкою бере System.currentTimeMillis() і кладе його як є. У результаті exp виходить у 1000 разів більшим за очікуване, токен виглядає «валідним» сотні років, а ви випадково винайшли перепустку безсмертя. Якщо хочете жити спокійно, використовуйте Instant.now().getEpochSecond() і Instant.ofEpochSecond(...), і тримайте мілісекунди подалі від claims.
Помилка № 2: використовувати локальний час (LocalDateTime) без часового поясу.
LocalDateTime виглядає дружньо, але в безпеці він часто перетворюється на пастку: у нього немає зони, а значить він не є «абсолютним моментом часу». У токенах і перевірках часу майже завжди потрібні Instant або ZonedDateTime в UTC. Інакше ви ризикуєте отримати токени, які «вчора» на одному сервері і «завтра» на іншому, особливо під час переходів на літній час і за різних налаштувань JVM.
Помилка № 3: сприймати nbf як «необовʼязкове, значить можна ігнорувати».
Якщо ви домовилися, що у вашому контракті nbf є і має сенс, то ігнорувати його не можна: інакше ви самі руйнуєте часову модель токена. Навіть якщо в навчальному проєкті nbf дорівнюватиме iat, важливо не загубити зміст поля: воно відповідає за нижню межу, і його відсутність або хибне тлумачення призводить до дивних «токен ще не активний» або, навпаки, до передчасного прийняття токена.
Помилка № 4: робити TTL величезним «заради зручності», особливо на старті.
Дуже хочеться поставити TTL «на день» або «на місяць», щоб під час розробки нічого не протухало. Але це непомітно ламає ментальну модель: ви перестаєте відчувати, що токен узагалі обмежений у часі, і починаєте проєктувати систему так, ніби «токен вічний». Краще зробити TTL розумним, а незручності розробки вирішувати свідомо (наприклад, налаштуванням TTL через конфігурацію), ніж виробити неправильну звичку.
Помилка № 5: забути про clock skew і отримати «випадкові» відмови на рівному місці.
У навчальному середовищі все часто крутиться на одному ноутбуці, і здається, що годинники завжди збігаються. Але варто зʼявитися кільком сервісам, контейнерам або просто віртуальній машині — і раптом частина токенів починає «ще не діяти» або «вже вважатися простроченими» на кілька секунд. Якщо ви перевіряєте час строго, ви ловите нестабільність. Якщо вводите малий skew (у межах кількох секунд), ви перетворюєте це на передбачувану поведінку.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ