В этой статье мы реализуем JWT авторизацию с использованием Spting-Boot приложений.
Теперь, когда мы знаем что такое JWT токен и как он устроен, пришло время использовать наши теоретические знания на практике.
Одно из преимуществ авторизации с использованием JWT – это возможность выделить выдачу токенов и работу с данными пользователей в отдельное приложение, переложив механизм валидации токенов на клиентские приложения. Этот механизм отлично подходит микросервисной архитекутуре.
Сначала мы сделаем приложение, которое будет совмещать в себе API и выдачу токенов. А чуть позже мы сделаем отдельное приложение, которое будет только валидировать токены, но не будет их выдавать. Таких приложений может быть много.
Схема авторизации будет такая:
В этой статье будет чистый REST-сервис без фронтенда. Подразумевается, что фронтенд написан отдельно: например, на каком-нибудь JavaScript-фрейворке.
Создание сервера авторизации
Я уже собрал начальную конфигурацию приложения на сайте start.spring.io, вам остается только скачать ее, распаковать, и открыть с помощью Idea.
После этого в pom.xml добавим еще 2 зависимости. Они понадобятся нам для генерации JWT токенов.
Сперва создадим наш незамысловатый API. У нас будет 2 роли для пользователей и два endpoints:
Теперь создадим класс пользователя системы User и enum отвечающий за роль пользователя:
В конструктор мы передаем секретные ключи, для подписи и валидации токенов.
Один будет использоваться для генерации access токен, а второй для генерации refresh токен. Приложение К будет знать ключ от access токена, так как ему нужно валидировать поступающие ему токены. Но он не будет знать ключ от refresh токен, а значит если Приложение К будет скомпрометировано, то мы просто заменим ключ от access токена, и не придется разлогинивать всех пользователей из-за того что их refresh токены станут не валидны.
Строки 20-26 отвечают за непосредственное создание access токена. В токен мы указываем логин пользователя, дату до которой токен валиден, алгоритм шифрования и наши произвольные claims: роли и имя пользователя. Для примера мы добавили в токен информацию об имени пользователя, чтобы на фронте не запрашивать бэкенд каждый раз, когда его нужно вывести.
Метод generateRefreshToken делает все тоже самое, только мы не передаем туда clains и указываем большее время жизни.
Сохранять их не обязательно, но это дает некоторые преимущества. Если каким-то образом злоумышленник заполучит секретный ключ для генерации refresh токенов, он не сможет создавать токены. Потому что ему нужно будет знать время создания токена конкретным пользователем.
Если бы сохранения не было, то он бы мог сгенерировать любой токен для любого пользователя, получить по нему access токены, и творить беспредел в системе.
Но учтите, если у вас есть сайт и мобильное приложение, то вам нужно будет сохранять два refresh токена для одного пользователя.
Еще один плюс, вы можете забанить пользователя в системе и отозвать его refresh токен, реализовав удаление токена из хранилища сохраненных и запрет на выдачу новых забаненым пользователям. В другом случае пользователь смог бы выпускать себе новые access токены, пока не протух бы refresh токен.
Тут ничего не обычного.
Также для большой безопасности мы защитим /api/auth/refresh и будем принимать на него запросы только с валидным access токеном. Для этого нам нужно создать класс конфигурации для настройки Spting Security.
Эндпойнт, который выдает токены по логину и паролю, а также тот, что выдает новый access токен по refresh токену, мы оставляем без авторизации. Остальные ендпойнты будут доступны только авторизированным пользователям (строки 17-19).
С сервером авторизации все, он готов.
Проверка работы
Вернемся к нашему проверочному контроллеру. Проверять работу сервера авторизации будем через Postman.
Первым делом получаем по логину и паролю access и refresh токены.
После этого берем access токен и вcтавляем его на вкладке авторизации, выбрав типом «Bearer token».
Далее выполняем запрос и получаем заветный результат.
Также попробуем получить сообщение предназначенное только для админов.
Видим 403 ошибку доступа, все работает как задумано. Если подождать 5 минут, то и по эндпойнту /api/hello/user увидим ту же самую ошибку, так как access токен протух.
Все работает. А теперь взамен текущего refresh токена получим новые access и refresh токены. Для этого вызовем /api/auth/refresh и передадим наш текущий refresh токен в теле запроса. Также не стоит забывать, что это защищенный метод, поэтому во вкладке Authorization вставляем наш новый access токен.
Отлично, мы получили новую пару токенов. А что если попробовать снова вызвать этот же запрос?
Мы получим ошибку, так как этого токена больше нет в сохраненных.
На этом основная часть закончена. Мы реализовали JWT авторизацию. Теперь вы запросто реализуете приложение клиент, если это необходимо. Нужно только оставьть функционал проверки токенов и авторизацию, и убрать функционал по выдаче новых токенов. Для удобства я сделал одно небольшое приложение клиент для демонстрации.
Склонируйте его себе и запустите. Токены выдает наш сервер авторизации, а приложение клиент может их использовать для доступа к своему API.
В старых системах управления пользователями, таких как Membership API, приложение считается авторитетным источником всей информации о пользователе. По сути приложение рассматривается как некий «закрытый мир» и система аутентификации доверяет данным, содержащимся в нем. Это укоренившийся подход к разработке программного обеспечения, который вы могли видеть на примере нашего приложения — мы аутентифицируем пользователей на основе учетных записей, хранящихся в базе данных и предоставляем доступ на основе ролей, связанных с этими учетными записями.
ASP.NET Identity предоставляет также альтернативный подход для работы с пользователями, который хорошо работает в рамках MVC-приложения, использующего несколько источников информации о пользователях, и который может быть использован для авторизации пользователей более гибким способом, чем традиционные роли.
Зачем нужно использовать?
Утверждения обеспечивают гибкий подход к системе авторизации. В отличие от ролей, утверждения авторизуют пользователя на основе информации, которая описывает этого пользователя.
Как использовать в рамках MVC?
Утверждения не используются непосредственно в рамка платформы MVC, но они интегрированы в стандартный функционал авторизации, такой как атрибут Authorize.
Описание концепции утверждений
— это информация о пользователе, а также информация откуда взялась эта информация (да, получилась тавтология). В ASP.NET Identity оно представлено классом Claim. Самым простым способом описать утверждения, является наглядный пример. Для начала добавьте в проект контроллер Claims со следующим содержимым:
Вы можете по-разному получить данные об утверждениях. В этом примере мы использовали свойство Claims, определенное в классе пользователя ClaimsIdentity. Экземпляр этого класса можно получить из свойства Identity объекта IPrincipal, который, в свою очередь, можно получить из свойства HttpContext.User. Свойство Identity возвращает реализацию IIdentity, которая является объектом ClaimsIdentity в ASP.NET Identity. Следующая таблица содержит описание свойств и методов этого класса:
Свойства и методы класса ClaimsIdentity
Название
Описание
Claims
Возвращает коллекцию объектов Claim, описывающих утверждения для пользователя.
Добавляет новое утверждение к информации о пользователе.
Добавляет список утверждений к информации о пользователе.
Возвращает true, если информация о пользователе содержит утверждение с заданным предикатом.
Удаляет утверждение для пользователя.
Перечисленные в таблице члены класса ClaimsIdentity, являются самыми широко используемыми. С помощью этих свойств и методов я продемонстрирую, как утверждения вписываются в более общую платформу ASP.NET.
В примере выше мы привели реализацию IIdentity к типу ClaimsIdentity, и вернули коллекцию объектов Claim в представление с помощью свойства Claims. Объект Claim описывает один элемент данных о пользователе и его свойства перечислены в таблице ниже:
Свойства класса Claim
Название
Описание
Issuer
Возвращает название источника, откуда поступают данные о пользователе
Возвращает базовый объект ClaimsIdentity, связанный с текущим утверждением
Возвращает тип информации об утверждении
Возвращает информацию об утверждении
Давайте используем эти свойства в представлении Index.cshtml, которое добавим в папку /Views/Claims. Это представление содержит таблицу с описанием каждого утверждения, связанного с пользователем:
Свойство Claim.Type возвращает URI для схемы Microsoft, который не очень полезен. Популярные схемы описываются специальным классом ClaimTypes из пространства имен System.Security.Claims. Чтобы извлечь полезную информацию из объектов этого класса, в представлении Index.cshtml мы использовали вспомогательный метод Html ClaimType(). Реализацию этого метода необходимо добавить в файл IdentityHelper.cs, содержащий расширяющие методы для класса HtmlHelper:
Теперь запустите наше приложение и перейдите по адресу /Claims/Index, предварительно авторизовавшись в приложении:
В этой таблице приведены базовые утверждения, которые автоматически добавляются при реализации системы аутентификации ASP.NET Identity, как в нашем приложении. Обратите внимание, что некоторые утверждения основаны на идентификаторе пользователя (Name содержит имя пользователя, а NameIdentifier его уникальный идентификатор в базе данных).
Также утверждения показывают членство в ролях — в таблице показано два утверждения типа Role, которые отражают тот факт, что пользователь «Елена» находится в ролях Users и Employees. Утверждение IdentityProvider описывает систему, с помощью которой был аутентифицирован пользователь — в нашем случае ASP.NET Identity.
Свойство Issuer описывает источник, откуда поступают данные о требованиях. В нашем случает это свойство содержит значение LOCAL AUTHORITY, которое говорит о том, что данные пользователя устанавливаются приложением.
Итак, теперь, когда вы увидели некоторые примеры утверждений, я могу описать что же они из себя представляют. Утверждение содержит любую информацию о пользователе, доступную для применения в приложении, например, идентификатор пользователя или членство в ролях. Как вы видели из примера, эта информация автоматически формируется, когда мы добавляем какой-то функционал в приложение. Например, когда мы добавили систему ролей, платформа ASP.NET Identity автоматически создала утверждения типа Role для каждого из пользователей.
Создание и использование утверждений
Как говорилось ранее, приложение может получать данные пользователя из разных источников, а не полагаться только на локальную базу данных Identity. Давайте добавим файл класса LoacationClaimsProvide.cs в папку Infrastructure нашего проекта:
Метод GetClaims() принимает параметр типа ClaimsIdentity и использует свойство Name для создания утверждений, описывающих почтовый индекс и область проживания. В реальном приложении эти данные могут быть получены, например, из базы данных HR, никак не связанной с текущим приложением.
Утверждения, связанные с идентификацией пользователя, можно добавить в процессе аутентификации. В примере ниже мы изменили метод действия Login контроллера Account, и добавили список утверждений с помощью метода LocationClaimsProvide.GetClaims():
Вы можете увидеть эффект от применения утверждений о местоположении пользователя, если запустите приложение, авторизуетесь и перейдете по адресу /Claims/Index:
Получение утверждений из нескольких источников означает, что приложение не должно дублировать данные, сохраняемые в другом месте и позволяет легко интегрироваться со сторонними системами. Свойство Claim.Issure всегда подскажет вам, в какой системе было создано утверждение, благодаря чему можно судить о точности получаемых данных. Например, данные о местоположении сотрудников, полученные из центральной базы данных HR, вероятно, будут более точными и надежными по сравнению с данными, полученными от внешнего поставщика списка рассылки.
Применение утверждений
Используя утверждения, можно более гибко настроить авторизацию в вашем приложении, нежели чем с ролями. Проблема с ролями заключается в том, что они статичны, и после того, как пользователю была назначена роль, он остается членом этой роли пока явно не будет удален из нее. Утверждения могут быть использованы для авторизации пользователей непосредственно на основе той информации, что известна о них. Самым простым способом реализации такой системы авторизации является генерация требований Role на основе пользовательских данных. Давайте добавим файл ClaimsRoles.cs в папку Infrastructure проекта со следующим содержимым:
Метод CreateRolesFromClaims() использует лямбда-выражение для определения того, имеет ли пользователь утверждение о местоположении StateOrProvince поступающее из источника RemoteClaims со значением «DC» (проживает ли пользователь в штате Вашингтон) и проверяет утверждение Role со значением «Employees» (является ли сотрудником компании). Если пользователь соответствует этим утверждениям, то ему задается роль-утверждение DCStaff. Следующий код демонстрирует как мы будем использовать метод CreateRoleFromClaims() в методе действия Login контроллера Account:
Теперь мы можем ограничить доступ к определенным методам с помощью новой роли:
Пользователи смогут получить доступ к методу действия OtherAction() только если их утверждения соответствуют членству в роли DCStaff. Членство в этой роли генерируется динамически, поэтому изменение данных о должности пользователя или его местоположения будет автоматически отражаться на авторизации.
Добавление фильтра авторизации
Предыдущий пример является эффективной демонстрацией того, как разрешения могут быть использованы для точечной настройки авторизации, потому что мы создали специальную роль на основе разрешений, а затем ограничили доступ к определенным методам используя проверку на членство в этой роли. Более прямым и гибким подходом к реализации системы авторизации на основе разрешений, является создание настраиваемого атрибута фильтра авторизации.
Добавьте файл ClaimsAccessAttribute.cs в папку Infrastructure со следующим содержимым:
Класс атрибута в этом примере является производным от AuthorizeAttribute, который позволяет легко создавать пользовательские политики авторизации в приложении MVC, путем переопределения метода AuthorizeCore(). Наша реализация этого метода предоставляет доступ, если пользователь был аутентифицирован, реализация интерфейса IIdentity является объектом ClaimsIdentity (т. е. имеет поддержку разрешений) и пользователь соответствует разрешению, которое настраивается путем инициализации свойств Issue, ClaimType и Value атрибута ClaimsAccessAttribute.
Следующий пример демонстрирует использование этого атрибута к методу действия OtherAction() контроллера Claims:
Фильтр авторизации в примере выше гарантирует, что доступ к методу действия OtherAction получат лишь те пользователи, почтовый индекс которых равен «DC 20500».
При разработке приложений на стеке Microsoft для получения информации о текущем пользователе достаточно часто(точнее почти всегда) можно встретить такие участки кода или обертки над ними:
Целью этих вызовов может являться необходимость принятия решения об авторизации вызова какой-то функции или метода, отображение информации о текущем пользователе и тд.
Информация о пользователе появлялась в этих классах разными способами: чтение данных из БД, Forms authentication, NTLM token, Kerberos token. В каждом конкретном случае решалась задача получения информации о пользователе, его аутентификации и получения дополнительной информации.
В “дооблачные времена” этого было вполне достаточно для большинства приложений. Если этого не хватало, то создавались разного рода собственные фреймворки, но во главе угла зачастую стоял главный вопрос: обладает ли пользователь определенной ролью. До определенного момента этого хватало пока хранилище пользователей было одно, не было необходимости взаимодействовать с партнерами по бизнесу и тд. С появлением облаков, распределенных систем, SaaS приложений и других плюшек без которых трудно себе представить современный веб, этой модели стало не хватать, если, к примеру, вы захотели разрешить сотрудникам вашего партнера доступ к определенным функциям вашей CRM. Так же часто встает вопрос развития и эволюционирования приложения, например: изначально вы планировали использовать две группы пользователей User и Administrator и в своем коде щедро раставили авторизационные атрибуты вида:
[Authorize(«Administrators»)] public ActionResult DoSomeHardcoreAdminStuff() < . >
а через год бизнес решил что неплохо было бы иметь несколько различных групп пользователей с разным уровнем доступа и ко всему прочему(ну что бы понять весь драматизм данной ситуации) разграничить права для администраторов на SystemAdministrator и SecurityAdministrator. И это не предел, так как эти требования ограничиваются лишь фантазией бизнеса.
С точки зрения разработчика, все это выливалось в зоопарк технологий и костылей. Каждое приложение аутентифицировало пользователей по-своему. Пользователь мог аутентифицироваться используя OAuth, Forms, Windows или что-нибудь еще. В каждом конкретном случае приходилось писать свою логику аутентификации и авторизации, а если у вас был Api, то еще и для него велосипедик дорисовать.
В ответ на это в 2008 году из недр Microsoft увидел свет первый релиз Windows Identity Foundation(WIF) и была представлена концепция Claims-based identity. Целью этого фреймворка является предоставление абстрактного механизма выражения своих требований к пользователю не углубляясь в детали того, как это работает.
Subject, тоесть вы, идете к Identity provider(паспортный стол) и на основе Свидетельство_о_рожденииToken получаете ПасспортToken. Потом, вместе с этим ПасспортToken вы идете к Relying party(кинотеатр) и, после подтверждения вашего возраста, получаете доступ к услуге.
Основные идеи которые можно извлечь из этого примера:
1. Для авторизации вас, как посетителя сеанса для взрослых, кинотеатру не надо вести свою базу клиентов или обращаться куда-либо. Ему достаточно вашего удостоверения личности которому он доверяет(Паспорт, военный билет, права).
3. С паспортом вы можете купить себе хорошего виски после похода в кино, взять ипотеку или еще что-то в любом учреждении, которое доверяет документам, выданным гос. учреждением.
Кто то уже работал с такими протоколами как OAuth, WS-Trust и WS-Fed, SAML-P и эта схема взаимодействия будет им знакома. Вкратце — информацию о пользователе вы получаете от доверенной удостоверяющей стороны(Identity provider) в виде токена определенного формата и используете ее для принятия каких-либо решений в вашем приложении. В вырожденном случае, например Forms authentication вы сами являетесь этой удостоверяющей стороной и сами же используете эту информацию. WIF допускает такие сценарии. WIF достаточно гибок для поддержки самого разного рода сценариев.
WIF позволяет “аутсорсить” процесс аутентификации доверенной стороне и позволяет свести к минимуму необходимость вмешательство разработчика в процесс аутентификации и авторизации. Все удостоверения, которые предъявляются вашему приложению приводятся к типам ClaimsPrincipal и ClaimsIdentity. Эти типы очень похожи на стандартные *Principal и *Identity, так же реализуют интерфейсы IPrincipal и IIdentity, но имеют дополнительное свойство, которое является коллекцией всех утверждений которые доступны вам о текущем пользователе. Причем для совместимости поддерживаются различные существующие способы работы с IIdentity и IClaimsPrincipal, например:
[PrincipalPermission(SecurityAction.Demand, Role = «Administrators»)] static void CheckAdministrator() < Console.WriteLine(«User is an administrator»); >
Для этого достаточно что бы у пользователя было утверждение типа роль( можно настраивать тип утверждений которые будут использоваться как роли) со значением “Administrators”.
В приложение на ASP.NET MVC это может выглядеть так:
[ClaimsAuthorize(ClaimTypes.Role, «Administrators»)] public ActionResult DoSomeHardcoreAdminStuff() < . >
[ClaimsAuthorize(ClaimTypes.Permission, «DoSomeHardcoreAdminStuff»)] public ActionResult DoSomeHardcoreAdminStuff() < . >
Есть и более сложные сценарии для проверки доступа к тому или иному ресурсу, или просто получение возраста пользователя, его почтового адреса, номера домашнего телефона.
В результате всех этих трансформаций вашему приложению больше не навязывается Role based security подход и вы вольны сами выбирать как, на основание чего и где проводить необходимые проверки, а так же, в части случаев, полностью избавиться от механизмов хранения информации о пользователе внутри приложения. Кроме всего прочего, вы не заботитесь о том, каким способом пользователь прошел аутентификацию, будь это стандартная пара логин-пароль или хитрая смарт карта. Это задача вашего Identity Provider.
На данный момент на платформах от Microsoft есть два основных решения для такого подхода: ADFS(Active Directory Federation Services) и Azure ACS. Если вам не подходит ни то ни другое, то вы вольны самостоятельно написать свой сервис, благо из коробки в студию ставится шаблон с примером. Так же есть опенсорсный сервер IdentityServer на основе которого можно развивать свой собственный продукт.
Из коробки WIF поддерживает следующие протоколы: 1. WS-Federation 2. WS-Trust 3. WS-Security 4. WS-SecurityPolicy 5. WS-Addressing
Поддержка протокола SAML-P находится в состоянии CTP. Информации о RTM версии пока нет. Так же есть OAuth2 extensions.
Стандартно поддерживаются удостоверения SAML1.1 и SAML2. Но уже есть достаточно развитые библиотеки которые добавляют поддержку SWT и даже JWT(Json Web Token).
Привет. Видел на форуме слово клайм, но постеснялся спросить что это такое.
Теперь жутко мучает любопытство. Яндыкс выдает ахинею.
Так что это за зверь и какая этимология этого слова?
Есть еще какой местный слэнг? Может нужно уже о словарике подумать?
Привет. Видел на форуме слово клайм, но постеснялся спросить что это такое.
Теперь жутко мучает любопытство. Яндыкс выдает ахинею.
Так что это за зверь и какая этимология этого слова?
Есть еще какой местный слэнг? Может нужно уже о словарике подумать?
А по аглицки набрать религия не позволяет?
Ващет и сам непонел.
Пожалуйста Войдите или Зарегистрируйтесь для того чтобы увидеть скрытое содержание
Ващет и сам непонел. Может; это поможет.
Пожалуйста Войдите или Зарегистрируйтесь для того чтобы увидеть скрытое содержание
Класно протролил..А сколько еще незнакомых словей-то.
Вот бы узнать Что такое реферер, рефер и реферал. Для некоторых это одно и тоже.
Сообщения типа: +реф., +реп., спасибо, работает, класс, проект фуфло и т.д. считаю за флуд. Буду ставить минус в репу, когда мне захочется. Без обид, надоело.
Кто из Казахстана, Может у кого есть без дела кулер на 775 для 4-х ядерника.
Класно протролил..А сколько еще незнакомых словей-то.
Вот бы узнать Что такое реферер, рефер и реферал. Для некоторых это одно и тоже.
Человек же задал простой вопрос.
Человек же задал простой вопрос.
Все самообучаются по-немногу: не стоит каждому долбить об этом в каждом посте, если у самого это дерьмо уже стало немного пониже рта.