Чаму системы Concurrent 401 выводзяць корыстнікаў з аккаунтаў, і як паспеліваць гэтую проблему
Як паралельныя запросы і зміна токену апдэта ствараюць супернічанне, якое прыводзіць да неспакоўных выходаў з акаунта, і як адна спяльная обяцанне апдэта ў інтэрсептары Axios запобегае гэтаму.
Корыстнік наўсёды ў паўпрацэзе заповнення дужа длігай формы, калі прыложэнне вяртае яго на экран аблогінацыі. Не бяў ні якога абэкту, ні зламу, і весь тое, што ён запісаў, зникло. Логіка выканання тэрміну дзейнасці токена выглядае правильна, процес апавяржэння таксама, пры тым сесіі продовжваюць заканчвацца рана. Чытаячы гэты текст, вы пазнаеце, чаму так адбываецца, калі калькі прыказаў атакуюць тэрмінаваны токен у адной момент, чаму баг так важка знайсці праз чытанне коду, і як невялікая змяна ў інтэрцептары Axios, якая дазволяе аднарадова выкорыстоўваць обяву апавяржэння, дапамагае пазбавіцца гэтай проблемы.
Сімптам: аблогінацыі, якія не павінны быць можлівымі
Уявіце сабэксплуатацыйную платформу, якаю выкарыстоўваецца працавнікамі на месцы. Спецыялісты вводзяць данні аб пераглядах чыму-небудзь аб экспертызах на планшетах, часта через ненадзеяныя мобільныя з’ўязкі. Прыходзіць адчыт, а потым ўжо іншы ад іншага пользователя: прыстанок вывелі яго з системы, калі ён запоўняў форму. У такой системе гэта не проста дробны недагодзін — гэта можа значыць неабходнасць перапісвавання двадзяці хвілін рэтельна введаных дадзеных.
Явным падазроўнікам є выкаанчэнне тэрміну дзейнасці токена, і на першы погляд гэта здаецца невінаватым. Токены доступу дзейнаць 15 хвілін. Калі запыт API вяртае код 401, кліент вызывае ендпоінт абнавлення, зберагае новы токен і продовжвае работу. Нічога з гэтага опису не павінна завершваць сесію ў разгар выкарыстоўвання. Калі явнае поясненне падтверджваецца, правільным крокам є перестаць паверяцца своему розумеўню коду і відтворыць адзначанне.
Відтворэнне адзначання, вызванага выкаанчэннем тэрміну дзейнасці токена
Гэты баг практычна з’яўляецца толькі за аднаго ўмовы: калькі API-запытав выкананыя праз малая часова інтервал, адночасна з моментам выканання тэрміну дачыны доступу. Форма з калькамі яўляецца ідеяльным фактарам, які гэты баг спрычынае — ён можа адправіць запыт на перакананне, запыт на автасаўванне і падтверджэння статусу заваносу файлу прыблізна за калькі мілісекунд адночасна. Якщо тэрмін дачыны выканаецца прыблізна за гэты час, усе запыты практычна адночасна вяртаюць код 401.
Інтэрцептор быў напісан для ситуацыі з адным запытам: пад кодом 401 вызываецца канечная точка апавежэння, прымаема новая тэрмін дачыны, і запыт практычна перзапускаецца. Калі тэставаўся з адным запытам за раз, ён працуе ідеяльна. Аднак калкі чатыры запыты выканаюцца без успеху адночасна, ён запускае чатыры незалежныя запыты на апавежэння.
Это супрацьходзіць з разумным рашэннем сервера: абранне новага токена для апдэта. Калі викорыстоўваецца токен для апдэта, сервер яго анулюе і выдае новы, таму вкрадзены токен не можа быць викорыстоўваны нескончаема. Тепер розглянем чатыры паралельныя спробы апдэта:
- Першая спроба апдэта успехоўна і практычна новы токен для апдэта.
- Другая, троцья і чатыртая спробы вже былі адправлены з старым токенам для апдэта, прычым першай адпаведзі сервера.
- Сервер іх адклёкае, таму што гэты токен яшчо не быў анульваны.
- Інтэрцептор спрыямае невыпанню апдэта як „сесія справжня ў канцы“ і выключае праграму пользователя.
Трымвочны час ліцэнзыяў ніколі не быў проблемай. Неправыя падазры заключаліся ў тым, што процесы апавяржэння ліцэнзыяў ніколи не вядуцься адночасна. Багатыя рэалізацыі механізмаў змены ліцэнзыяў ідуць да таго, што вважаюць павторны выкарыстоўванне старой ліцэнзыі знакам крадзіжкі, і тады анулююць усю групу ліцэнзыяў, чым тая ж сама проблема ператвараецца на ўсё бол агрэсіўны процес выйшчы з системы.
Чаму чытанне коду не паказала гэтага
Можна сказаць, што гэты урок ценнейшы за самае рашэнне проблемы. Якщо чытаць з верху да нізу, інтэрсептар працюе правільна: ловіць код 401, апавяржваць ліцэнзыю, прабаваць знову. Гэта тая самая последоваснасць, для якой ён быў напісаны, і павторнае чытанне толькі падтверджае яе.
Але чытанне коду не паказвае таго, што інтэрсептар працюе не аднаразова. Ён працюе аднаразова за кожны невялікі запит, і гэтыя запыты вядуцься адночасна, а не па серіі. Дыбаггаванне яго як лінейнага скрыпта значыць рассуждання пра тое, што робіць кожны крок, ігнаруючы час яго выканання.
Шаблон становіцца видным практычна адразу, калі вы фіксаваеце час запуску на кожны вызов апдэта. У такім сценарыі вы пабачыце чатыры спробы апдэта прыблізна за 40 мілісекунд адна ад другой, усе якія спрямованы да таго ж канцэнтра. Гадзіну часу, якую можна пасвятаваць аналізу логіки, можна заменіць двумя хвілінамі аналізу часоў. Калі, за кодам, баг «не можа выйсці», аналіз порядку і часу выканання запускаеў часта єсць быстрэй, чым зноў чытаць код.
Рашэнне: адзін запуск у процесе, усі іншы чакаюць
Калі справжня прычына проблемы стане ясной, спосаб её рашэння ўскора. У змену таму, каб кожны 401 запускаў сваёе апавяржэнне, інтэраптар пераканальваецца, чы не ведзецца ўжо апавяржэнне. Як толькі такое ведзецца, новая памылка не запускае іншае; ён чакае на тую ж самую упэўненасць і прабуе зноў, калі гэтая упэўненасць будзе вырашана. Гэта часта называецца шаблонам адной польотнай місіі.
Першы элемент — это зменная на рэвэлі модуля, якая храніць апавяржэнне, якое ведзецца, або null, калі жаднага апавяржэння немае:
let refreshPromise = null;
Ніжшы адрабатар вызываецца для запытку, які не выйшаў з кодам 401. Якщо refreshPromise ў яго пусты, ён вызывае refreshAccessToken() і зберагае рэзультатны праміс, прыкрепляючы до яго блок finally, які перазначае зменную на null незалежна ад таго, чы спрацаваў процес апавяржэння, чы ні, ў тым разе наступны термін выгорання можа запусціць новую спробу. Кожны вызывальнік, першы і всі наступныя, чакае гэты ж праміс, задае новы токен-носіцель у адпаведную настройку запытку і практычна перадае запытка знову через axios.
async function handleUnauthorized(originalRequest) {
if (!refreshPromise) {
refreshPromise = refreshAccessToken().finally(() => {
refreshPromise = null;
});
} const newToken = await refreshPromise;
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return axios(originalRequest);
}
Сынтаксіс мае меншое значэнне, чым сама ідея: адна спільная обяцанне, на якую чакаюць усі нездачлівыя запиты, замест таго каб кожны з іх намагаўся апавярнуць даны. Першы код 401 запускае процес апавярнення; будзь-які код 401, який прыходзіць пад час ўжо трываючага процесу, выкарыстоўвае ўжо наявны рэзультат, замест таго каб зноў викорыстаць токен апавярнення. Паколькі JavaScript выкананае на адной нітка, перакрыцце і прызначэнне refreshPromise не можа відбывацца паралельна між рознымі запитамі, і самэ гэта робіць такі просты механізм захавання достатнім.
Есць калькі деталей, якія варта прыняць у выважанне, калі інтегруваць гэта ў рэальны перехоплювач:
- Якщо сам процес апавярнення зазнае нездачы, усі чакаючы запиты адхіляюцца разам, тады прыложэнне можа адбыць адзін чысты процес выйшчы з аккаунта, замест калькі супернікаючых процэсаў.
_retry у налашчаннях), ў результате чаго запыт, які знову падае з кодам 401 пасля апавярнення, не будзе ціклічна паўтарыцца без канца.401 з канцэнтра павторнага запыту спробуе апавярніць ся сам.Па вопыту сервернай часткі таго ж дизайна, уключаючы ротацыю та анулювання, адзірніце нашыя інструкцыі па стратэгіі refresh token для систем аутантыкацыі Node.js.
Галоўныя вывары
Пасля введення можлівасці кардынальнага апдэта за адзіны рух падзеяць зупыніліся. Большая частка змян — це перакананне: спрыяць да таго, каб сумаверныя запыткі былі стандартным разлікам, а не выключэнным случаем, які трэба рашыць пазней. Калі вы пішаеце інтэрсептар, кялейку чыста ўсё працэўнік, який реагуе на асінхронныя падбілы, запытайце ся, што будзе, якщо ён запрацюе чатыры разы за пяцідзесят мілісэкунд. Трафік у працоўнай супэрвізыі ад актываўных форм і нестабільных мобільных з’яносоў раней чы пазней створыць такую ситуацыю.
- Баг на самай справе не стосаваўся токэнаў апдэта; ён стосаваўся коду, які прыпускаў, што западзеяння выкананыя адно за другім.
- Ротацыя токэнаў апдэта — гарная практыка безпекі, і саме яна выявляе дублікатныя запыткі апдэта.
- Код, який здаецца правільным, калі чытаць лінія за лініяй, можа ўсё равно зламацца пад сумавернасцю; фіксуйце часы западзеяння, каб бачыць, калі ўсё выканана, а не толькі што выканана.
finally і захавацеся ад цыклів падзеяў.Супаўязаныя матэрыялы
- Як адразу впрыснуць токены доступу і павынавлення ў Node.js — Дазвольце вам дазнацца, як у Node.js спаўнаваць сумешанне токенав доступу з мяркавым терміном дзейнасці і токенав павынавлення, каб збалансаваць безпеку і плавнае ведама ўжоў.
- Чаму калебраныя функцыі у React useEffect не павінны быць асінхроннымі — Дазвольце вам дазнацца, чаму вярненне асінхронной функцыі з useEffect наражае на рызык правільнае функцыонаванне React, і пазнакоміцца з чатырма правильнымі спосабамі безпечнага адкалекватавання асінхронной логікі.
- Express і Axios: апдэйт токена, стежыце за запыткамі по адзінам — Створыце процес доступу і апдэйту JWT з абакентам Express і кліентам Axios, пасля стежыце за тым, як код 401 ператвараецца ў спяльны процес апдэйту токена і прозрачную пракушанню.