Три года назад мне поручили задачу по распилу сразу двух монолитов с вынесением в общий микросервис данных пользователей и созданием единого механизма аутентификации и авторизации. Конечной целью этого действа было создание надежной базы для перехода всей разработки в команде на микросервисную архитектуру.
Тогда-то я впервые и применил на практике JWT, результатами чего остался очень доволен. В этой статье я хочу поделиться своим опытом работы с этим стандартом на конкретных примерах и рассказать, почему он удовлетворяет все мои потребности как разработчика в самых разных проектах: новых и долгоживущих, микросервисных и монолитных, в ситуациях, когда нужно быстро накидать MVP, и когда требуется не спеша и вдумчиво заложить основу на долгие годы качественной разработки.
Как мы обычно делаем Security
До того, как поставленная задача вынудила меня использовать JWT, я реализовывал Security по стандартному flow, описываемому в документациях фреймворков.
Как правило, первым шагом мы описываем класс User, который несет в себе атрибуты, необходимые для принятия решения о доступе. Эти атрибуты могут помещаться:
в плоской структуре самого класса
User(просто делаем полеroleи подобные поля);в агрегируемых юзером классах, таких как
Profile;в смежных классах, ссылающихся на
User, например, классеManager, описывающем уровни и полномочия менеджеров.
Далее мы описываем систему ролей, пермишенов и правил контроля доступа. Иными словами, пишем логику авторизации: тот код, в котором происходит принятие решений о доступе на основании атрибутов пользователя и других параметров, таких как совершаемое действие, атрибуты ресурса, над которым совершается действие и т. п.
После этого мы связываем тем или иным способом компонент фреймворка Security с нашим классом User и кодом, содержащим логику авторизации. Детали здесь уже зависят от фреймворка и подхода разработчика, но концептуально механизм выглядит плюс-минус одинаково:
Бэкенд получает некие credentials: токен, пару логин-пароль или сессионные куки.
По этим credentials из хранилища - чаще всего реляционной БД - извлекается инстанс нашего класса
User. Этот инстанс обычно доступен через компонент фреймворка Security, то есть, в коде мы можем сделать что-то вроде$security->getLoggedUser().В зависимости от запрашиваемого действия (чаще всего это выполнение того или иного контроллера) вызывается определенный код проверки прав доступа, куда передается либо весь
User, либо его отдельные атрибуты.Если код авторизации не прервал выполнение - то есть, было принято положительное решение о доступе - продолжается обычная работа приложения: выполняется запрашиваемое действие.
Недавно я писал о том, как вот такая работа с 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 обязан содержать всю необходимую для принятия решения о доступе информацию. Если следовать этому правилу, то логично предположить, что нам потребуется класс, несущий эту инфромацию, и класс, собирающий ее (нет, не фабрика, фабрика - это другое, и хватит называть этим словом любой порождающий код
