Три года назад мне поручили задачу по распилу сразу двух монолитов с вынесением в общий микросервис данных пользователей и созданием единого механизма аутентификации и авторизации. Конечной целью этого действа было создание надежной базы для перехода всей разработки в команде на микросервисную архитектуру.

Тогда-то я впервые и применил на практике JWT, результатами чего остался очень доволен. В этой статье я хочу поделиться своим опытом работы с этим стандартом на конкретных примерах и рассказать, почему он удовлетворяет все мои потребности как разработчика в самых разных проектах: новых и долгоживущих, микросервисных и монолитных, в ситуациях, когда нужно быстро накидать MVP, и когда требуется не спеша и вдумчиво заложить основу на долгие годы качественной разработки.

Как мы обычно делаем Security

До того, как поставленная задача вынудила меня использовать JWT, я реализовывал Security по стандартному flow, описываемому в документациях фреймворков.

Как правило, первым шагом мы описываем класс User, который несет в себе атрибуты, необходимые для принятия решения о доступе. Эти атрибуты могут помещаться:

  • в плоской структуре самого класса User (просто делаем поле role и подобные поля);

  • в агрегируемых юзером классах, таких как Profile;

  • в смежных классах, ссылающихся на User, например, классе Manager, описывающем уровни и полномочия менеджеров.

Далее мы описываем систему ролей, пермишенов и правил контроля доступа. Иными словами, пишем логику авторизации: тот код, в котором происходит принятие решений о доступе на основании атрибутов пользователя и других параметров, таких как совершаемое действие, атрибуты ресурса, над которым совершается действие и т. п.

После этого мы связываем тем или иным способом компонент фреймворка Security с нашим классом User и кодом, содержащим логику авторизации. Детали здесь уже зависят от фреймворка и подхода разработчика, но концептуально механизм выглядит плюс-минус одинаково:

  1. Бэкенд получает некие credentials: токен, пару логин-пароль или сессионные куки.

  2. По этим credentials из хранилища - чаще всего реляционной БД - извлекается инстанс нашего класса User. Этот инстанс обычно доступен через компонент фреймворка Security, то есть, в коде мы можем сделать что-то вроде $security->getLoggedUser().

  3. В зависимости от запрашиваемого действия (чаще всего это выполнение того или иного контроллера) вызывается определенный код проверки прав доступа, куда передается либо весь User, либо его отдельные атрибуты.

  4. Если код авторизации не прервал выполнение - то есть, было принято положительное решение о доступе - продолжается обычная работа приложения: выполняется запрашиваемое действие.

Недавно я писал о том, как вот такая работа с Security превращается в боль и о том, какими средствами можно этому противодействовать. Одним из таких средств как раз является JWT.

Схемы применения этого стандарта различаются для монолита и микросервисов. Поэтому сейчас я поделюсь с вами как монолитным, так и микросервисным кейсом применения JWT. Но прежде мне бы хотелось остановиться на тех свойствах JWT, которые делают его таким полезным для построения более качественной архитектуры приложения.

Два полезных свойства JWT

Когда я прочитал некоторое количество из бесчисленных материалов о том, что такое JWT, из каких частей он состоит и т. д., мне показались ключевыми два его свойства: автономность и способность нести payload.

На самом деле, речь идет о двух сторонах одной медали: JWT автономен потому, что он несет payload. Чем это нам помогает? Прежде всего тем, что JWT может и должен содержать в себе всю необходимую для принятия решения о доступе информацию. Иными словами, если ваше приложение получило JWT, оно уже не должно дергать юзера из базы или (упаси Боже) из соседнего микросервиса.

Первая выгода, которую несет автономность JWT, лежит на поверхности: уменьшается количество запросов к базе и/или в соседние микросервисы. Но, когда я начал внедрять JWT, то осознал и другую выгоду. Стандарт как бы навязал мне более правильную архитектуру.

Когда я начал думать о том, как мне сформировать payload для моего JWT, то быстро осознал: payload JWT - это не мой доменный User. Это отдельная структура, которая может содержать в себе данные не только из класса User, но и из других источников. Именно возможность вынести в JWT все, что необходимо для работы Security, ощутимо развязывает руки при проектировании нашего приложения.

Конечно, у автономности JWT есть и оборотная сторона: однажды выданный токен не может быть отозван и продолжает действовать пока не протухнет. На самом деле, это решаемая проблема. Некоторые решают ее через сохранение отозванных JWT, но это в корне неверно. Свое решение я опишу в конце статьи.

Теперь, когда мы разобрались, чем JWT так полезен, давайте я поделюсь своим опытом использования его в монолите и микросервисах.

JWT в монолите: делаем несвязанный слой Security

При проектировании Security приложения я исхожу из того, что JWT payload обязан содержать всю необходимую для принятия решения о доступе информацию. Если следовать этому правилу, то логично предположить, что нам потребуется класс, несущий эту инфромацию, и класс, собирающий ее (нет, не фабрика, фабрика - это другое, и хватит называть этим словом любой порождающий код ?).

Если класс, необходимый для JWT payload, несет в себе все, что нужно для авторизации, то логично завязать всю работу Security фреймворка именно на этот класс. Я обычно именую этот класс SecurityUser. Не так давно я уже писал о нем, приводя пример кода.

Если вы вынесете из доменного User все, что касается работы Security, то увидите, как ваш слой бизнес-логики стал чистым и независящим от инфраструктурных компонентов. В тоже время вы получите возможность более независимо развивать оба класса: Domain\User и SecurityUser. Давайте я покажу это на схемах.

Схема 1
Схема 1

На первой схеме изображено, как происходит аутентификация и выдача токена. Вы можете видеть, что класс Domain\User выступает только источником данных для SecurityUser и ничего не знает о слое Security. В то же время, слой Security, взяв необходимые данные из Domain\User, сразу же “забывает” о нем, никак с ним более не пересекаясь.

Я намеренно избегаю на схеме указаний на какой-либо конкретный фреймворк, приводя универсальное решение. В конце статьи я дам ссылку на пример кода с использованием Symfony, чтобы дополнить обе эти схемы.

Схема 2
Схема 2

На второй схеме изображено использование ранее выданного JWT, которым будут подписываться все запросы к нашему приложению. Как видите, в этой схеме вообще отсутствует доменный User. Мы десериализуем JWT payload в SecurityUser и далее весь слой Security фреймворка работает именно с этим классом, передавая его экземпляр в контроллер и код с логикой авторизации.

Под кодом с логикой авторизации может пониматься следующее:

  • Код, который пишется непосредственно в контроллере, что-нибудь вроде симфонийского $this->denyAccessUnlessGranted().

  • Код, выполняющийся до контроллера, например иишный beforeAction() или симфонийский Voter.

За последние три года я применил решение, изображенное на этих двух схемах, в нескольких проектах и остался им очень доволен, видя в нем следующие преимущества:

  • Четкое разделение доменного и инфраструктурного слоев.

  • Много точек расширения: классы Domain\User и SecurityUser можно развивать независимо. PayloadGenerator и JWTManager так же могут дорабатываться по мере необходимости.

  • Авторизация по JWT совместима с любой формой аутентификации: по логин-паролю или, например, с помощью сторонних сервисов. В примере кода к этой статье показана аутентификация с помощью виджета Telegram.

  • Если когда-либо потребуется переход с монолитной архитектуры на микросервисную, будь то распил монолита, или просто добавление новых сервисов, слой Security вашего приложения уже готов к этому. Вам ничего не придется переделывать.

  • Разгружается хранилище: теперь не нужно в каждом запросе к приложению лазать в базу за данными, необходимыми для принятия решения о доступе.

Будучи полезным и удобным в монолитных приложениях, в микросервисах, я считаю, JWT становится просто незаменим. В качестве примера я приведу тот самый проект, о котором писал вначале статьи.

JWT в микросервисах: единый токен для всех запросов

Если говорить о технических деталях, то решение по применению JWT в микросервисах базируется на решении для монолитов. Выдача токена и его использование для авторизации происходит по тем же схемам. Отличие состоит в том, что за выдачу токена отвечает отдельный сервис, а использовать выданный токен для авторизации может любой из наших сервисов.

Ключевым моментом здесь является соблюдение все того же правила: JWT обязан нести всю, необходимую для принятия решения о доступе, информацию. Если это правило соблюдается, то любой ваш сервис или микросервис, получив запрос, подписанный JWT, сможет самостоятельно принять решение о доступе, не запрашивая данные пользователя: ведь он уже получил их из JWT payload.

Причем неважно, откуда поступает запрос к микросервису: с фронта, API-шлюза или из другого микросервиса. Главное, передавать в этом запросе JWT. То есть, если у вас есть цепочка запросов от одного сервиса к другому (что не очень гуд, но встречается повсеместно), то вы просто перекладываете JWT из одного запроса в другой.

В более сложной схеме логика авторизации сосредоточивается в отдельном сервисе (не в том, где данные пользователя). Такое возможно, если вы, например, реализуете ABAC.

В качестве примера приведу вам схему решения той задачи, о которой я писал вначале статьи: вынос из двух монолитов аутентификации и авторизации в отдельные сервисы.

Схема 3
Схема 3

На этой схеме изображено пошаговое прохождение HTTP запроса через несколько сервисов с использованием единого JWT.

  1. С фронта приходит запрос с логином и паролем на аутентификацию.

  2. API шлюз переадресует этот запрос в сервис “Паспорт”.

  3. “Паспорт” генерирует JWT (см. выше вторую схему из раздела о монолите).

  4. JWT возвращается фронту, фронт его запоминает.

  5. С фронта поступает подписанный JWT запрос на удаление рекламной кампании.

  6. Запрос переадресуется шлюзом в сервис кампаний.

  7. Сервис кампаний передает JWT из запроса в сервис ABAC для принятия решения о доступе.

  8. Сервис ABAC без обращения к “Паспорту”, опираясь только на данные из JWT payload, принимает положительное решение о доступе.

  9. Сервис кампаний удаляет кампанию и посылает запрос в сервис биллинга для пересчета баланса пользователя. Запрос к биллингу содержит все тот же JWT, полученный от фронта.

  10. Биллинг пересчитывает баланс и возвращает успешный ответ сервису кампаний.

  11. Сервис кампаний возвращает успешный ответ API шлюзу.

  12. API шлюз отправляет успешный ответ фронту.

В более простой конфигурации у вас не будет API шлюза и/или сервиса ABAC. Сервисы кампаний и биллинга будут самостоятельно принимать решения о доступе, используя данные из JWT payload, без обращения к “Паспорту”.

Плюсы такого решения по применению JWT в микросервисах те же, что и при применении в монолитах. В коде каждого сервиса вы получаете возможность развести доменные и инфраструктурные слои. Чтобы жилось еще проще, нужно вынести в отдельный composer пакет класс SecurityUser и остальные классы, необходимые для работы с JWT.

Этот пакет должен быть в зависимостях каждого вашего сервиса, которому требуется авторизация для выполнения каких-либо действий.

Вот, собственно, и все, чем я сегодня хотел поделиться с вам из своей практики. Как и обещал, в конце статьи опишу свое решение проблемы отзыва JWT и еще пары моментов, которые портят удовольствие от применения этого стандарта.

Решение проблем с обратной совместимостью и отзывом JWT

У главного плюса JWT - автономности - есть и оборотная сторона: однажды выданный токен обязан приниматься приложением до тех пор, пока не истек срок его действия. Это порождает две проблемы: обратной совместимости и отзыва токена.

Проблема обратной совместимости возникает тогда, когда мы расширяем наш SecurityUser новыми данными и дорабатываем код авторизации, который опирается на эти данные. Как только этот код попадает в прод, все выданные ранее токены начнут ронять приложение еще на этапе десериализации JWT payload с ошибкой Undefined array key или подобной.

Чтобы избежать этой проблемы, нам следует таким образом писать наш код десериализации JWT payload, чтобы при нехватке в токене каких-либо полей, клиенту возвращался отказ в доступе. Это может быть обычный ответ 401 или спецэффичный для данного конкретного случая ответ. Фронт, получив такой ответ, должен перезапросить актуальный JWT по refresh токену.

Что до проблемы отзыва токена, то она возникает, как правило, в двух случаях: пользователь был по тем или иным причинам заблокирован, или же изменились права доступа. Причем неважно, стало у пользователя больше прав, или меньше: все ранее выданные JWT теперь могут приводить к принятию неправильного решения о доступе.

В интернете я встречал решение проблемы отзыва JWT, основывающееся на сохранении выданных токенов. Мне такое решение мягко говоря не нравится, потому что оно нивелирует преимущества JWT. Ведь какой путь проходит информация, чтобы мы в итоге получили свой токен?

  • Делается запрос в базу.

  • Полученные данные гидрируются в сущность Domain\User.

  • Из Domain\User данные переливаются в SecurityUser.

  • Из SecurityUser данные переливаются в массив (нормализация, вот картинка, если что).

  • Из массива данные перекидываются в JSON строку (кодирование, см. картинку из предыдущего пункта).

  • К полученному JSON (который и есть payload) добавляются дополнительные поля, диктуемые стандартом JWT.

  • Полученная структура подписывается.

  • Все это добро передается в base64_encode().

Итого, что получается? Мы достаем данные из базы, вытворяем над ними все вот эти действия и… Кладем их обратно в базу. Так ведь мы проходили все эти шаги для того, чтобы больше не трогать базу по этому поводу. Я считаю, если разработчик решил сохранять JWT, то ему стоит подумать об отказе от JWT. Я говорю это без насмешки или иронии. Любое решение несет плюсы (дает выгоду) и минусы (имеет цену). Если вы отказываетесь от плюсов, то за что платите цену? Не проще тогда каждый раз дергать юзера из базы, минуя все остальные шаги, которые я перечислил выше?

Если нельзя сохранять весь токен, то что тогда делать? К сожалению, хранилище задействовать все-таки придется. Только вместо сохранения JWT мы будем хранить userId и timestamp, начиная с которого все выданные ранее токены считаются недействительными. Если вы фанат key-value хранилищ, то вам точно понравится такой подход.)

Работает это так: если права юзера были изменены, или юзер был заблокирован, мы сохраняем временную метку этого события вместе с id юзера. Теперь при каждом запросе, получив токен, мы достаем из хранилища по userId временную метку, начиная с которой все JWT этого пользователя считаются протухшими. Если в хранилище ничего нет, продолжаем работу. Если метка есть, сравниваем ее с timestamp в самом JWT. Если JWT выдан до хранимого нами таймштампа, то отвечаем отказом в доступе.

Согласитесь, хранить по одной метке на каждого юзера, чьи права изменились, лучше, чем хранить все выданные JWT для всех пользователей системы. Хранилище этих меток можно автоматически подчищать со временем, удаляя те метки, которые старше заданного вами срока действия JWT. Если вы используете key-value хранилище (что вовсе необязательно), то вы можете просто задавать TTL.

Обратите внимание: timestamp в самом JWT и в хранилище “меток протухания” должен быть с микросекундами. Это нужно для ситуаций, когда фронт посылает запрос, изменяющий права пользователя и следом посылает другой запрос. Например, когда пользователь вступает в некую группу, что открывает ему доступ к материалам этой группы.

Я сталкивался с проблемой, когда фронт посылал такой запрос и сразу же посылал следующий запрос на просмотр чего-либо. В ответ он ожидаемо получал 401: ведь все JWT юзера были отозваны. Получив отказ в доступе по протухшему токену, фронт мгновенно его перезапрашивал и стучался уже с новым токеном. И получал второй отказ: время в секундах у нового токена совпадало со временем, когда были изменены права пользователя. Добавление микросекунд к временным меткам решило эту проблему.

Заключение и пример кода

В заключение, пожалуй скажу пару слов о данных, которые вы складываете в JWT payload. Если вы подписываете свой токен (для этого лучше использовать проверенные библиотеки и не городить свой огород), то подменить какие-либо поля у злоумышленника не получится.

Если же вы переживаете, что злоумышленник раскроет структуру вашего проекта, узнав, например, некоторые значения из вашего enum Status, то вы можете заменить все строковые значения числовыми константами, а само поле внутри payload назвать не статусом, а бананом.

Лично я таким не занимаюсь, но периодически слышу от коллег, что как же так, в JWT payload можно глазками прочитать все поля, нехорошо это. Как будто в инструментах разработчика браузера нельзя сделать то же самое, читая запросы к бэку и ответы. Вы также можете использовать какое-нибудь запредельно простенькое обратимое шифрование всего вашего payload, чтобы защититься от прочтения его глазами. Непростенькое шифрование скушает время обработки запроса, что сведет на нет все преимущества JWT.

Последняя вещь, которую я хотел бы добавить - это то, что JWT payload со временем может разрастаться вплоть до того, чтобы упереться в лимит по размеру HTTP заголовка. Проблема решается все той же заменой строковых значений на числовые, что сделает JSON компактнее. Дополнительно вы можете зазиповать свой токен, например, с помощью bzcompress(). Это, опять же, защитит его от умников, которые попытаются сделать base64_decode(), и, раскрыв ваши енамы, взломают вам всю систему ?.

Ну а пример кода, для фреймворка Symfony я выложил в специальном посте в своем канале. Что поделать, надо же как-то продвигаться.) Прошу вас, дорогие читатели, не серчать по этому поводу, а лучше подписаться на канал или, хотя бы, проголосовать за тему следующей статьи: согласитесь, чем больше читателей, тем интереснее мне, как автору, что-то писать для вас.)

Кстати, решение для симофни опирается на lexik/jwt-authentication-bundle, который реализует подписывание JWT SSL ключами и интеграцию с Symfony Security. Если вы не нашли для своего фреймворка похожей библиотеки, то я рекомендую вам позаимствовать классы из этого пакета и на их базе доработать собственное решение. Спасибо, что дочитали до конца.)

Комментарии (29)


  1. ednersky
    13.02.2025 11:12

    есть противоречие:

    • хотим выдать токен и чтоб пользователь год с ним ходил (условная авторизация на сайте)

    • но хотим иметь возможность пользователя забанить

    чтобы его решить, нужно вспомнить: а зачем мы вообще в токены полезли?

    а ответ такой: чтобы не ходить в БД при обработке КАЖДОГО запроса пользователя.

    • А почему плохо ходить в БД на каждый запрос пользователя?

    • А потому что нагрузка на БД получается сумасшедшая (равна сумме всех-всех-всех запросов)

    Размышляя над вот этим вот всем, мы пришли к следующему компромиссу:

    • нужно делать время жизни токена небольшим (минуты). Тогда забаненный пользователь сможет несколько минут поработать и это ok.

    • перегенерацию новых токенов нужно проводить без привлечения пользователей (то есть ему не нужно заново попадать на страницу "логин")

    То есть что мы делаем?

    1. в токене указываем два времени:

      1. до которого он действителен - +1 год

      2. после которого нужно выполнить верификацию валидности/забаненности итп

    2. on-demand периодически валидируем все токены, обновляя второе время в них (то есть перевыдавая Set_Cookie)

    Получается и на базу нагрузка радикально снижена, и хранилище забаненых не нужно... и волки сыты и овцы целы, в общем.


    1. slayervc Автор
      13.02.2025 11:12

      Есть ощущение, что вы прочитали статью очень сильно по диагонали. Время жизни токена вы задаете сами: у меня это, как правило, две недели. Про сумасшедшую нагрузку на базу я тоже не писал. Статья в принципе не об этом.

      1. в токене указываем два времени:

        1. до которого он действителен - +1 год

        2. после которого нужно выполнить верификацию валидности/забаненности итп

      2. on-demand периодически валидируем все токены, обновляя второе время в них (то есть перевыдавая Set_Cookie)


      Вы точно мою статью пересказываете? Придумали какие-то годы и минуты и перевалидацию всех токенов.


      1. ednersky
        13.02.2025 11:12

        я попытался дополнить Вас своим опытом. Вы предложили одно решение с отзывом JWT, и поругали другое. У меня третье - я подумал, Вам может быть интересно.


        1. slayervc Автор
          13.02.2025 11:12

          Прошу прощения. Я подумал, что Вы критикуете меня в форме искаженного пересказа моего текста.) Теперь, когда Вы указали, что это Вы описали свое, третье, решение, я взглянул на Ваш комментарий по-новому. Мой косяк.)

          Ваше решение тоже интересно, еще подумаю над ним. Идея проверять на отзыв JWT не при каждом запросе, а раз в несколько минут интересна, тут, конечно, бизнес-требования каждого конкретного проекта будут вносить свои коррективы.

          Единственно, что хотелось бы отметить, так это то, что годовалый срок жизни токена считается плохой практикой с точки зрения информационной безопасности.

          Спасибо большое, что поделились своим решением.)


          1. ednersky
            13.02.2025 11:12

            с точки зрения айти - год или месяц - разница слабая. для захода на ресурсы, вроде хабра, например, я б реально год и делал


            1. slayervc Автор
              13.02.2025 11:12

              Возможно, я просто опять Вас не так понял и мы снова говорим про разное.) Обычная схема какая: Выдается два токена: собственно JWT и refresh_token. Когда срок жизни JWT истекает, фронт (или иной клиент) автоматически запрашивает новый токен по refresh токену.

              Предполагается, что если злоумышленник перехватит JWT, то он сможет пользоваться им только до истечения его срока годности, а потом, не имея refresh токена, он потеряет доступ к приложению.

              При таких условиях чем меньше срок жизни JWT, тем считается безопаснее. При этом автоматический перезапрос JWT по refresh токену, осуществляемый фронтом, проходит для пользователя незаметно: его не выкидывает на страницу логина.


              1. supercat1337
                13.02.2025 11:12

                Кстати, а как насчёт привязки токена к IP-адресу? Плюс для безопасности можно проверять данные юзерагента.


                1. slayervc Автор
                  13.02.2025 11:12

                  JWT, как правило, выдаются людям, а люди редко имеют статические IP-адреса. Технически-то легко сделать: добавляете IP в payload токена и проверяете при каждом запросе. Только смысл это будет иметь, если клиентами вашего приложения являются не люди, а другие приложения, работающие на статических IP-адресах.


                  1. ednersky
                    13.02.2025 11:12

                    ну вообще, я захожу на хабр - я залогине

                    включаю VPN - рефреш - хабр показывает что я разлогинен

                    выключаю VPN - рефреш - я снова залогинен

                    то есть какая-то связь с IP есть, только кажется не с IP а с регионом


              1. ednersky
                13.02.2025 11:12

                Когда срок жизни JWT истекает, фронт (или иной клиент) автоматически запрашивает новый токен по refresh токену.

                да, идея с рефреш токеном работает, но она предполагает логическую нагрузку на клиента: он должен бдить за протуханием токена, затем перевыпускать его.

                при этом перевыпуск - это обычный поход в БД и если Вы на это всё посмотрите, то увидите, что вариант с refresh токеном и тот, что предложил я по сути одинаковы (для пользователя поведение сайта совершенно такое же), но с refresh-токеном имеем большую сложность клиента:

                • нужно хранить два токена, а не один

                • нужно уметь токен перезапрашивать by другой токен

                если токен живёт в cookie, то можно всю автоматизацию свести к прокси-серверу (который итак есть), верифицирующему токены, который будет периодами выдавать новый set_cookie и всё. А клиент становится простым: получает либо 200 Ok и всё хорошо, либо 401 и редиректит клиента на логин.


                1. slayervc Автор
                  13.02.2025 11:12

                  Ваши комментарии еще поосмысляю, спасибо, что пишите их. Пока пару моментов могу сказать:

                  Перевыпуск токена на фронте можно сделать без слежки за временем, а просто как реакцию на 401 ответ. Фронты на это спокойно смотрят.)

                  На счет проверки токена на необходимость отзыва раз в n минут. Это не будет работать в ситуации, которую я описал в статье: юзер вступил в группу по приглашению и сразу редиректится на страницу со списком материалов группы. При проверке токена раз в n минут юзер будет ждать эти самые n минут, получая 403 в ответ на попытки увидеть материалы группы.

                  То есть, здесь надо будет более тонкую логику какую-то делать. Пока еще соображаю.)


                  1. ednersky
                    13.02.2025 11:12

                    здесь речь не о банах, а о доступе к ресурсу, который зависит от сохранённого в JWT состояния.

                    смена роли пользователя, если она кешируется в JWT, изменение групп - если она кешируется в JWT.

                    Все эти проблемы будут всплывать одинаково.

                    Мне кажется, что здесь дешевле НЕ придумывать централизованный механизм, а давать пользователю что-то нажать, чтобы "войти в группу", вот ту ссылку, например. И она может перегенерить токен.

                    Ещё, например, в токене можно хранить ФИО пользователя. И это невероятно удобно: везде где требуется его имя написать - берём из токена и радуемся (и иконку оттуда же).

                    Но если мы так делаем, то в том месте, где пользователь своё ФИО редактирует, приходится перегенеривать и токен.

                    В общем, это обычная проблема инвалидации кеша.

                    как там в меме было? в IT существует всего две неразрешимые (спорные) проблемы: инвалидация кеша и правильное название идентификаторов.


                    1. slayervc Автор
                      13.02.2025 11:12

                      Отвечать новым токеном на запрос о вступлении в группу - хорошая идея! Спасибо!


                      1. slayervc Автор
                        13.02.2025 11:12

                        Единственно, если фронт по каким-то причинам не сохранит полученный JWT, то будет ломиться со старым токеном. Так что, уйти таким способом от проверки актуальности токена, как я вначале подумал, не получится, увы.

                        В любом случае, Вы накинули свежего взгляда, есть над чем подумать.)


                      1. ednersky
                        13.02.2025 11:12

                        в этом месте открытый вопрос: где хранить токены? в Cookie - самый простой и надёжный вариант.

                        а если в нём, тогда set-kookie может выдаваться любой ручкой, в любое время. Огород с отдельными ручками и не обязателен.


                      1. slayervc Автор
                        13.02.2025 11:12

                        Да, только если делать set-cookie в ответе на каждый запрос, то мы возвращаемся к началу: придется каждый раз генерировать JWT, лазая в базу.


                      1. ednersky
                        13.02.2025 11:12

                        не не, конечно "можно генерить где угодно" не стоит сводить к "нужно генерить везде".

                        теоретически, отдавая 401/403 можно было бы ходить в базу (рефрешить токен), но такой подход может быть рассмотрен как уязвимость.

                        вообще любой URL приводящий к рефрешу токена через базу, если база имеет установленную мощность рассчитанную не на FullRPS, а на FullRPS/timeout, надо рассматривать как потенциальную уязвимость.

                        но в этом месте снова философия:

                        • индеец Джо неуловим потому что...

                        • vs когда нас будут атаковать, тогда и подумаем. такого ведь может и никогда не случиться, а если таки происходит, то это признак успеха - не грех и в железо вложиться.

                        Вообще on-demand вложения в железо куда лучше (как по мне), нежели "заранее чтоб БД всю нагрузку держала". Ибо деньги лишними не бывают.


                  1. ednersky
                    13.02.2025 11:12

                    Перевыпуск токена на фронте можно сделать без слежки за временем, а просто как реакцию на 401 ответ. Фронты на это спокойно смотрят.)

                    Ага, но здесь сложность фронта разная:

                    • или на каждый 401/403 просто показывать "доступ запрещён"

                    • или разделять 401/403:

                      • здесь доступ запрещён, потому что пользователь прав не имеет

                      • а здесь потому что токен сэкспайрился и надо его отрефрешить, а потом перезапросить

                    Если у нас фронт уже написан - вопросов нет. Любое решение подходит.

                    а если мы с нуля новый проект начинаем, то можно снять часть работы с фронтендеров.

                    как-то так.


              1. ednersky
                13.02.2025 11:12

                Предполагается, что если злоумышленник перехватит JWT, то он сможет пользоваться им только до истечения его срока годности, а потом, не имея refresh токена, он потеряет доступ к приложению.

                если злоумышленник может перехватить JWT, то что-то скомпроментировано:

                • либо злоумышленник имеет доступ к браузеру (тогда взять refresh токен ему не составит труда)

                • либо он расшифровывает SSL и MTM атакой видит всё, в т.ч. рефреш-токены.

                в общем, не вижу пользы от refresh-токена, кроме усложнения.

                то есть польза от него есть, если мы JWT вкладываем в URL'ы, а пользователи друг дружке URL'ы пересылают. Но, кажется, в этом случае имеет место быть архитектурная ошибка.


      1. supercat1337
        13.02.2025 11:12

        Аналогично я не понял тезис о бешеной нагрузки на базу. А для чего вообще такая база нужна тогда, если не выдерживает нагрузку? Проверка подлинности должна быть на стороне сервера. Если пользователь забанен, значит должен быть забанен здесь и сейчас.


        1. ednersky
          13.02.2025 11:12

          Аналогично я не понял тезис о бешеной нагрузки на базу. А для чего вообще такая база нужна тогда, если не выдерживает нагрузку?

          эм, ну как бы да. но представьте, вы - владелец ресурса. У вас пользователи делают 1000.000 запросов в секунду. Для проверки авторизации можно поставить выделенный сервис или базу данных, например YDB/Монга, которая этот трафик выдержит.

          При этом, такой сервис скорее всего будет представлять собой кластер. Ну или Tarantool или Redis, который тоже столько выдержит, но в общем случае тоже будет представлять кластер, на меньшее число инстансов, но всё же кластер. Да ещё и IN-MEM, то есть дорогой.

          Если Вы будете рассматривать отвергнутую автором идею - хранения токенов в БД, то обращений к этой БД тоже будет 1000.000RPS. Если вместо токенов сохранять в БД только user_id, то количество RPS тоже не меняется. Вся разница, что большинство запросов будут иметь "пусто" в ответе.

          Но если мы токен начнём проверять на валидность (то есть забаненность - проверка с походом в БД) не на каждый запрос, а например не чаще раза в 15 минут - 900 секунд, то тяжёлые 1000.000RPS у нас превращаются в уютные 1111 RPS. Которые выдерживает даже какой-нибудь простейший постгрис, монга или ещё что.

          И эту БД тупо можно даже не выделять, а как авторизационную использовать общую БД хранящую пользователей.

          как-то так

          Если пользователь забанен, значит должен быть забанен здесь и сейчас.

          это большой философский вопрос. Вот логическая цепочка

          1. банят пользователей, как правило, ручными манипуляциями - даже если банит робот, но он часто реагирует на жалобы людей итп. То есть в конечном итоге источником бана является человек.

          2. соответственно схема бана: человек (пусть даже искусственный) получил (или составил) уведомление о нарушении, рассмотрел его, забанил

          3. то есть между "возникла необходимость в бане" и самим баном в общем случае есть интервал времени, исчисляющийся минутами, а то и часами или даже днями.

          4. на этом фоне задержка вступления бана в силу в несколько минут рояли не играет


          1. supercat1337
            13.02.2025 11:12

            Спасибо за комментарий. Есть над чем подумать.


  1. vanxant
    13.02.2025 11:12

    Если в более-менее сложном проекте запихать в jwt всю информацию, необходимую хотя бы для допуска/недопуска юзера к функционалу и данным, может получится много. Прям килобайты, а то и десятки. Гонять такое на каждом запросе - ну такое себе.

    На самом деле, никакой трагедии в походе в базу по первичному ключу нет. С этим и обычные бд справляются, а если кажется что нет, то есть всякие редиски с монгами и тарантулами. В этом случае в jwt будет лежать только id юзера и его пропуска. Зато - снимается проблема с логаутами и банами. А вот подпись jwt позволит ваф (фаерволлу) отсекать дудосеров ещё до похода в базу.


    1. slayervc Автор
      13.02.2025 11:12

      На самом деле, никакой трагедии в походе в базу по первичному ключу нет.

      Да, все верно. Трагедии нет. Просто, как я писал, у каждого решения есть плюсы (профиты) и минусы (цена). Соответственно, не упомянуть об этом профите (уменьшение запросов в базу) при описании решения было бы, на мой взгляд, неправильно.)

      Прям килобайты, а то и десятки

      Да, есть такой минус у этого решения: к проектированию payload нужно подходить ответственно и думать о том, как сжимать токен. Способы для этого я упомянул.


  1. savostin
    13.02.2025 11:12

    Как много воды, даже устал немного. А про главное собственно и не сказали: что в этом payload хранить, чтоб было достаточно принимать решение по доступу к любому ресурсу и действию над ним. Имхо от этой информации он «сильно распухнет и будет мешать ходить». Образуются всякие роли, и каждый сервис должен их держать актуальными, хоть они и не часто меняются.


    1. slayervc Автор
      13.02.2025 11:12

      А про главное собственно и не сказали: что в этом payload хранить, чтоб было достаточно принимать решение

      Вспомнил мультик советский, где героя отправляли к двум дядькам из ларца.

      Вы хотите получить универсальное решение, чтобы можно было его в любой проект вставлять, и больше не думать об этом никогда. Если набор таких решений когда-то появится, то поголовье нас, разработчиков, сильно сократиться.

      Логика, на основе которой принимается решение о доступе, диктуется бизнесом. В одном проекте эта система ManagerAuthorities с разными видами и уровнями менеджеров. В другом - это система членств в разных группах. В третьем - система тегов, которыми овнеры(админы) размечают создаваемых ими юзеров.

      В этом и состоит работа разработчика: понять требования бизнеса, спроектировать решение и реализовать его так, чтобы было комфортно с ним жить как можно дольше.

      А вы пишите, что вы устали, читая статью и разочарованы тем, что ваш труд по прочтению не был вознагражден таким результатом, что вы навсегда будете избавлены от необходимости думать, на основании чего вам принимать решение.

      Образуются всякие роли

      Попробуйте мыслить атрибутами, а не ролями.

      каждый сервис должен их держать актуальными

      Обратите внимание на тот абзац, где я писал про вынос всего, что касается Security, в отдельный composer пакет.

      Вообще, пока писал ответ на ваш комментарий, вспомнил о тех ребятах, которые требуют им расписывать задачу до такой степени детализации, что сделать самому эту задачу выходит в несколько раз быстрее.


  1. boblgum
    13.02.2025 11:12

    IMHO
    Смешивать Security с Domain как-то попахивает. Тем более в таком порядке.
    1. Symfony не интересует ваш Domain.
    2. По определению у каждого микросервиса есть своя база данных. Т.е. только у одного есть (может/должна быть) информация о юзерах
    3. Понятие роли определяется в контексте определенного микросервиса

    В общем, если вы первым шагом запрашиваете Domain/User, то вы нарушаете Single Responsibility


    1. savostin
      13.02.2025 11:12

      О, интересное решение. И как управлять ролями во всех микросервисах?


    1. slayervc Автор
      13.02.2025 11:12

      Своя ролевая система для каждого микросервиса - звучит как путь, полный боли.) Одну-то тежяло в порядке поддерживать, не давая ей разрастаться.