Калі аутэнтыка JWT стаецца залежной ад стану: праблемы викорыстання сэсій на серверах у Node
Блакітныя спискі і функцыі апаверзу зберагача прымусова робяць аутентыкацыю за дапамою JWT станоўкай. Сесіі Express у Node.js, якія падтрымваюцца праз Postgres, є простэйшымі, і там, дзе JWT все ўсё падходзяць добра.
Крыніцы: Forget JSON Web Tokens: Server-Side Sessions Cut Our Auth Bugs By 70%
Аутантыка на асасе JWT без стэта выглядае элегантна на дошцы знакавання, але багато командаў выявляюць, што процесы выйшча з системы, прымусовага выйшча з системы і перазначэння пароляў таямна вяртаюць стэт. Калі ў гэты прыгодзе ўжо задействаваны блакітнік анулювання, хранэнне токенаў апавяржэння і спецыяльны мідлвэр, шар аутантыкацыя стае найболей кранкім элементам системы. У этой статыце рассказваецца пра гэты патерн абяканняў, паказваецца, як настройка сэсій на асасе Postgres у Express можа яго заменіць, і пояснююцца рэдкія случаў, калі JWT застаецца лепшым інструментам.
Знаёмы патерн абяканняў
Уявіце команду, яка паўзасабліваеся своім налаштаваннем JWT, пакуль не заповнюецца черга на падтрымку. Карыстальнікі выключаюцца адсутнае, скасаванні паролях перестаюць дзейсніцца, а адны кліент подае тры заявкі за один дзень, у кожнай з яых описваецца той самы парадокс: ён як бы заўвязаны, але водночас і не заўвязаны. Інжынеры витрачаюць чатыры гадзіны на адну такую заявку, декодуючы його токен і пераканальваючыся ў тым, што той не выйшаў з тэрміну дзейснасці, і ўсё выглядае правільна.
Такія багі зазвычай з’яўляюцца ў швах межаў разных механізмаў, кожны з якіх дадаўця для усунення лімітаў пакойшага.
Чаму „безстановы“ рэдка калі застаецца безстановым
Стандартны аргумент на захад JWT ўражаючы. Не трэба ні хранення сесій, ні пошуку ў базе дадзенаў: вы кладзеце ID карыстальніка ў пэйлоад, падпісваеце його, перадаеце кліенту і дазволяеце самам токену быць аднаго з джэрел правды.
Проблема заключаецца ў анулюванні. Падпісанный токен застаецца чынным пакуль не выйдзе за термін, а сервер не мае вбудованага спосабу для його ранньяга анулювання. Для выключэння пользователя з усіх систем, реагавання на крадзены токен чым анулювання доступу пасля змены пароля неабходна блакітная лістка токенав, якая ў самым дзеле є тым видам спільнага стану, якія планавалася усунуць.
У вышэўказаным сценарыі команда змушана викорыстоўваць блакітную лістку токенав у Redis для анульваных токенав, околачы стоў для токенав апавяржэння чым спецыяльны шар проміжных супрацоўкаў, які ўсе іх з’яеднае. Система, якая была адзначаная як безстановая, стала самым становым компонентам у архітэктуры.
У чым заканчываецца перагляд за кожны запит
Спрасцаваная версія мідлвэра, яка запускаецца з кожнага запиту, выглядае так. Яна выявляе токен-несучы гэты пакет з заголовка Authorization, шукае яго ў блакітніцы Redis і пасля таго пераверачае падпис і даныя токена за дапамою jwt.verify.
// verify access token
function verifyToken(req, res, next) {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ error: "No token" });
// check if revoked
const revoked = await redis.get(`revoked:${token}`);
if (revoked) return res.status(401).json({ error: "Token revoked" });
jwt.verify(token, process.env.SECRET, (err, decoded) => {
if (err) return res.status(401).json({ error: "Invalid token" });
req.user = decoded;
next();
});
}
У такам варыянце функцыя нават не будзе запускацца: ў яй викорыстоўваецца await без пазначэння як async. Большая проблема — архітектурная. Кожны запит тепер выконвае сецьярнавы вызов да Redis, таму цэнныя прынтры JWT, такія як адсутнасць патробу ў пошуку, зникаюць. Да таго дыяўуць таксы CPU, неабходныя для пераверкі токена, а таксама додатковыя вычысленні ўжо праз тэрмін дзейнасці і даныя токена.
Багі хаваюцца ў прычынках межы гэтых трох пераверк:
- Токен доступу выйшаў за тэрмін дзейнасці, і вызов на апавежанне пра падтрымку токена выконваецца без жадных сігналоў пра аберанне.
Кожны з гэтых прыбліжэнняў трэба практычна выправіць на разным роўні, а выправа на однам роўні можа лёгка зламаць падазрэння на іншым.
Замена трох механізмаў аднай базой сэсый
Альтернатыва — сэсіі з боку сервера. Кліент прытрымвае толькі нечытальны ідэнтыфікатор сэсый у куку, а даны сэсый знаходзяцца ў вашай базе дадзенаў. У Express express-session з храненнем connect-pg-simple зберагае сэсіі ў Postgres:
const session = require("express-session");
const PgSession = require("connect-pg-simple")(session);
app.use(session({
store: new PgSession({ pool: db }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
}
}));
Калькі настройкі виканаюць большую частку работ па абэранці. Атрыбут httpOnly не дазволяе JavaScript у сторанцы даўаць доступ да кукі, чым ускладнюе крадзенне токенаў через XSS. Атрыбут secure гарантуе, што кукі адправляюцца толькі через HTTPS. Значэнне resave: false запобегае збераганню незмененых сэсій па кожны запит, а saveUninitialized: false не дазволяе хранілцу заполняцца порожнімі сэсіямі анонімных візітараў. Значэнне maxAge задае сэсіям трымкі семі дзён. Заўважыце, што connect-pg-simple чакае, што табелі сэсій існуе, таму стварыце яе ў рамках сваіх майграцый.
Пасля гэтага аперацыі становяцца практычна простымі:
- Пры входзе задаёцца
req.session.userId. - Пры выходзе вызываецца
req.session.destroy(). - Абавязковы выход знішчае рэкі корыстніка з табелі сэсій.
Анулювання тепер адмініструецца простым выдаленням, без неабясненага заканчэння тымчасовага списку блакітных адрэсоў, і кожны запыт чытае тую ж самую автантывацыйную інфармацыю. Варта дадаць два заходы па забезпечэнню безпекі: вызваць req.session.regenerate() пад заходам, каб запобiec фіксацыі сэшна, і задаць параметр sameSite для кукі (а таксама захист пры CSRF, як гэта неабяснена), таму што кукі адправляюцца автаматычна браузерам, на вядмэнь ад заголовкаў-носіцелаў.
У описаным тут сцэнарыі міграцыя выдала прыблізна 340 рэядоў логікі автантывацыі, і кожны выдалены рэяд стаў меншай можлівасцю для сховання бага.
Аналіз змян
Каб справедліва оцаніць пераход, каманда стала фіксаваць кожны адзвешчанні пра багі, звязаныя з автантывацыёй, прыблізна 90 дней да міграцыі і 90 дней пасля яе. Їхныя рэзультаты:
Before (JWT):
- Auth bugs reported: 43
- Avg debug time per bug: 2.4 hours
- Force-logout capability: not possible
After (Sessions):
- Auth bugs reported: 13
- Avg debug time per bug: 0.6 hours
- Force-logout time: < 1 second (delete row)
Reduction: ~70%
Колькасць адказаў пра багі зменілася з 43 да 13, што станавіць апошнё 70% змены, а сярэдня вялічына часу на дыбаггін за адны баг знизілася з 2,4 гадзіны да 0,6. Прымусаванне выйсця з системы перастало быць немагчымым і стала процэсам выдалення рядка, які займае менш чым секунду. Это цифры адной команды, а не стандартны показнік.
Звычная працоўнае заперкі ёсць прабава да базы дадзеных за кожны запит. Для большасці прыкладоў праглед первачнага ключа з табелі сэсый ёсць недорогі па апэрабатаванню у порываў з запытамі, якія вже выкананы пад час звычайнага запиту, таму рэальныя узгорнутыя моменты практычна завжды знаходзяцца інде.
Порэванне двух шляхоў запыту
Якща паказаць іх падыяк, разліка ўсёўразумелая. У шляху JWT сервер дэкодавае токен, запытае Redis пра список заблакаваных адресоў і пераканаецца ў тым, чы не выйшла тэрмінавае працэсуванне і чы супакоўкі правядзяцца, прычым рухальнік маршруту ўжо працюе:
Client --> [Token in header] --> Server
|
Decode token (CPU)
|
Check Redis blocklist (network)
|
Validate expiry + claims
|
Route handler
У шляху сэсый сервер чытае сэсію з Postgres, а пасля перадае яе рухальніку маршруту:
Client --> [Session cookie] --> Server
|
Lookup session in Postgres
|
Route handler
Адзін пошук у адзіным абсалютным источнікам дакументаў значыць, што калі ў чымсь з’яўляюцца проблемы, існуе толькі адна пазицыя, яку трэба пераглядаць.
Дзе яшчэ паслужаюць JWT
Нічыга з гэтага не робіць JWT паказвальнымі. Яны ідуць у прынцыпе для токенав, якія існуюць коротка час і прызначены для конкрэтных цэлей: лінкі для паўерыфікаціі электронной пошты, процесы перазначэння пароляў і іншыя самодостатныя доказы намеру, якія выгораюць за кальколькі хвілін і ніколі не трэба анулюваць пад час ўжытку. Токен паўерыфікаціі, як той, што паказаны нижэй, несе ідэнтыфікатор пользователя і мету яго викорыстоўвання, і выгорае чераз 15 хвілін:
// email verification — expires fast, never needs revocation
function makeVerifyToken(userId) {
return jwt.sign({ userId, purpose: "verify" }, process.env.SECRET, {
expiresIn: "15m"
});
}
Указванне атрыбута purpose мае важнае значэнне, адколі гэта не дазволяе прыймаць токен верыфікацыі там, дзе патрабуецца іншы тип токена. Памятайце, што сама JWT не можа гарантаваць ексклюзыўнае вжыцце; якщо лінк для перазначэння павінен працаваць толькі раз, неабходна зафіксаваць, што ён вжыты, або прыяўзаць токен да значэння, якое змінюецца пасля вжыцтва, напрыклад, до хэша тэкучага пароля.
Спецыфікацыя ніколі не была проблемай. Проблема заключалася у тым, што яе прабавалі застосоўваць да стойкіх, можна анулюваных, дзейнавучых сесій корыстнікаў, для чаго ўсё-такі былі створеныя сесіі.
Чаму команды продовжаюць викорыстоўваць складную аутэнтыкацыю
Сесіі воспрімляюцца як старомодныя, а JWT — як сучасныя, і колькасць нарадчыкаў па JWT значна перавышае колькасць нарадчыкаў па сесіях. Гэта неравнавага вплывае на тое, што воспрімляецца як правильнае.
Аптэкты з большым стажам часта выбіраюць працоўныя, але нудныя рашэнні. Дастаткова колькасць інцидэнтав у працэсе вырабоцтва навучыла іх, што нудныя системы рэдка калі ствараюць проблемы вечарамі, а сесіі з боку сервера надзейна працавалі ўжо дзесяткі гадоў, задаўно перш чым з’явіўся Redis.
Выбір падходу
- Пачынаеце новы веб-дзял? Выберыце сесіі з боку сервера: кукі з атрыбутамі
httpOnly,secure,sameSite; сесіі зберагаюцца ў Postgres, апавяржваюцца праз запуск у системе і знішчаюцца пасля выхаду. Для болей шырокага порэвання паказваецца наша інструкцыя па выбору межы сесіямі і JWT-амі. - Існуе ў вас налажана система з JWT, якая працуе надзейна? Не трэба яе мяніць; міграцыя толькі для самой міграцыі прыносіць рызыкі.
Найлепшая інжынерная рашэнне часта ўсё тое, якае дазволяе адмахнуцца кода. Менш ліній означае менш месцаў для багоў, а меншыя шары дапамагаюць быстрэй знаходзіць засталыя багі.