Как реализовать JWT-аутентификацию и авторизацию в ASP.NET Core

JSON Web Token (JWT) - это компактная и безопасная для URL-адресов строка, которая представляет утверждения в определенном формате, который определен отраслевым стандартом RFC 7519. JWT - это стандартный метод безопасной передачи требований между двумя сторонами. Люди обычно используют JWT в качестве удостоверения личности в веб-приложениях и мобильных приложениях.

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

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

В этой статье я покажу вам, как реализовать приложение веб-API ASP.NET Core с использованием аутентификации и авторизации JWT. Это приложение веб-API реализует такие процессы, как вход в систему, выход из системы, токен обновления, олицетворение и т. Д. На следующем снимке экрана показаны конечные точки API, которые мы рассмотрим в этой статье.

Я разделяю свое решение на две части: интерфейсное приложение в Angular и внутреннее приложение в ASP.NET Core. Вы можете найти полное решение в моем репозитории GitHub. И интерфейсные, и внутренние приложения поддерживают Docker, и вы также можете запускать их одновременно в контейнерах Linux с помощью Docker Compose.

В этой статье мы сосредоточимся на серверном решении, которое включает два проекта: JwtAuthDemo и JwtAuthDemo.IntegrationTests. Проект интеграционного тестирования охватывает все обычные процессы JWT в проекте веб-API.

Как люди обычно используют JWT

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

  1. Пользователь отправляет учетные данные на веб-сайт для входа в систему.
  2. Серверная часть веб-сайта проверяет учетные данные, объявляет правильные утверждения, затем генерирует JWT и возвращает его пользователю.
  3. Пользователь удерживает JWT до истечения срока его действия и отправляет JWT на веб-сайт в последующих запросах.
  4. Веб-сайт проверяет JWT и решает, доступен ли ресурс, а затем обрабатывает запрос соответствующим образом.

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

Для простоты я пропустил процесс обновления токена в приведенном выше потоке. Обычно случайная строка, токен обновления, генерируется вместе с токеном доступа JWT на шаге 2. Когда срок действия токена доступа JWT истекает, клиент отправляет токен обновления на серверную сторону, чтобы получить новый токен доступа JWT. Рекомендуется, чтобы система возвращала новый токен обновления вместе с новым токеном доступа. Следовательно, у приложения больше нет долговечного токена обновления. Этот метод известен как Обновить ротацию токенов.

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

В демонстрационных целях мы создаем проект веб-API ASP.NET Core JwtAuthDemo и проект MS Test JwtAuthDemo.IntegrationTests. Сначала мы настроим аутентификацию JWT для нашего проекта веб-API. Затем мы реализуем процессы входа, выхода и обновления токенов.

Конфигурация аутентификации JWT

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

Свойство Secret - это строка, которую необходимо хранить в безопасном месте, например, переменные среды пользователя пула приложений, облачное хранилище секретов или хранилище ключей. AccessTokenExpiration и RefreshTokenExpiration - это два целых числа, представляющих общее время жизни токенов с момента их создания. Время указано в минутах в зависимости от реализации этого демонстрационного проекта. Для простоты мы сохраним параметры в файле appsettings.json. Затем мы готовы передать значения в конфигурации JWT Bearer.

Хорошая новость заключается в том, что аутентификация с помощью токенов JWT проста, и пакет Microsoft.AspNetCore.Authentication.JwtBearer NuGet выполняет большую часть работы за нас. После того, как мы установили последнюю версию этого пакета NuGet, у нас есть два варианта настройки аутентификации JWT: (1) использовать промежуточное ПО app.UseJwtBearerAuthentication() в методе Startup.Configure; (2) вызовите метод services.AddJwtBearer(), чтобы зарегистрировать схему аутентификации JWT в методе Startup.ConfigureServices.

Здесь мы настроим аутентификацию JWT Bearer через второй подход. В следующем фрагменте кода показан пример ConfigureServices метода.

В приведенном выше коде строки 2 и 3 считывают наши настройки и регистрируют JwtTokenConfig как Singleton в контейнере Dependency Injection (DI).

Строки с 5 по 8 устанавливают по умолчанию для схем аутентификации и проверки значение Bearer в этом приложении. Строки с 9 по 24 настраивают токен носителя JWT, особенно параметры проверки токена. Хочу отметить следующие атрибуты.

  • RequireHttpsMetadata: значение по умолчанию - true, что означает, что для аутентификации требуется HTTPS для адреса метаданных или полномочий.
  • SaveToken: значение по умолчанию - true, что сохраняет токен доступа JWT в текущем HttpContext, чтобы мы могли получить его, используя метод await HttpContext.GetTokenAsync(“Bearer”, “access_token”) или что-то подобное. Если мы хотим установить SaveToken равным false, мы можем сохранить токен доступа JWT в утверждениях, а затем получить его значение с помощью метода: User.FindFirst("access_token")?.Value.
  • TokenValidationParameters: этот объект устанавливает параметры, используемые для проверки токенов идентификации. Значение каждого свойства говорит само за себя. Одна вещь, которую я хочу упомянуть, - это свойство ClockSkew. Я установил его значение равным одной минуте, что дает время для проверки истечения срока действия токена. У меня есть интеграционный тест для этого свойства, и вы можете с ним поиграться.

Затем мы переходим и добавляем метод app.UseAuthentication() в метод Startup.Configure, как показано ниже.

Промежуточное ПО Authentication, строка 5, критически важно для того, чтобы зарегистрированные схемы аутентификации (в данном случае JWT Bearer) работали. С другой стороны, промежуточное ПО Authorization, строка 6, критически важно для того, чтобы зарегистрированные механизмы авторизации работали. В этом проекте мы используем авторизацию на основе ролей по умолчанию. Обе строки 5 и 6 необходимы, чтобы мы могли использовать атрибут [Authorize] над контроллерами и методами действий. Кроме того, обратите внимание, что порядок промежуточного программного обеспечения важен.

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

Генерация токенов и вход

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

В классе JwtAuthManager мы сохраняем словарь _usersRefreshTokens как кеш для токенов обновления. В качестве альтернативы мы можем сохранить токены обновления в базе данных или в распределенном кеш-хранилище. Хранение копии токенов обновления на стороне сервера позволяет системе проверять токены обновления и искать метаданные о пользовательских сеансах.

Метод GenerateTokens создает токен доступа JWT и токен обновления. Мы передаем утверждения пользователя в полезную нагрузку в токене доступа JWT и устанавливаем правильные значения для параметров проверки токена JWT. Токен обновления - это просто случайная строка, но мы также дополняем объект RefreshToken сроком действия и именем пользователя. Кроме того, мы можем прикрепить к объектам RefreshToken другие метаданные, например IP-адрес клиента, пользовательский агент, идентификатор устройства и т. Д., Чтобы мы могли идентифицировать и отслеживать пользовательские сеансы, а также обнаруживать мошеннические токены.

Предостережение. Обратите внимание, что в строках 17 и 20 токен не будет длинным, если он обновляется несколько раз. Причина, по которой токен JWT становится длиннее, заключается в том, что утверждение aud является массивом и продолжает добавлять к нему новые значения. Конечно, есть и другие способы сохранить aud претензию в чистоте.

Поскольку класс JwtAuthManager не имеет зависимостей Scoped или Transient, мы можем зарегистрировать его как Singleton в контейнере DI. Затем мы можем ввести JwtAuthManager в AccountController, который выполняет действие Login. В следующем фрагменте кода показаны методы действия AccountController и Login.

В приведенном выше коде мы сначала проверяем учетные данные для входа в строки с 26 по 29, используя UserService. Затем мы генерируем утверждения в строках с 31 по 36. Строка 38 вызывает метод GenerateTokens в классе JwtAuthManager, чтобы получить токен доступа и обновить токен. Наконец, метод Login возвращает объект с токенами на стороне клиента.

Выйти

После того, как токены JWT отправлены обратно клиенту, они сохраняются на стороне клиента. Когда клиент хочет выйти из системы, мы можем удалить токен, удалив токены в cookie или localStorage. Однако пользователь может по-прежнему иметь токен доступа. Обычно риск невелик, поскольку токен доступа истекает через небольшой промежуток времени. Если вы все еще хотите аннулировать токен доступа JWT на стороне сервера, вы можете прочитать больше в этом обсуждении StackOverflow и этой проблеме GitHub, где предлагается стратегия черного списка.

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

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

Обратите внимание, что удаление токенов обновления на основе имени пользователя не является идеальным решением, поскольку оно приведет к выходу из всех сеансов для этого пользователя, даже если пользователь использует два браузера или один на рабочем столе, а другой на мобильном устройстве. Поэтому, чтобы улучшить взаимодействие с пользователем, мы должны удалить только определенный токен (например, на основе пользовательского агента и IP-адреса клиента), который может быть идентифицирован в теле запроса или заголовках.

Обновите токен доступа JWT

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

В приведенном выше коде также проверяется исходный токен доступа, чтобы убедиться, что токен обновления и токен доступа связаны. Тело ответа API содержит объект (строки 18–24), аналогичный результату метода Login, так что утверждения и токены восстанавливаются должным образом. Если во время процесса обновления произойдет какое-либо исключение, API вернет Unauthorized код состояния HTTP (строки 26–29), чтобы клиентская сторона могла перенаправить пользователя на страницу входа.

Строка 16 - это место, где происходит волшебство. Реализация метода Refresh показана ниже.

В приведенном выше коде мы сначала декодируем токен доступа JWT, чтобы подтвердить, что мы получили подлинную личность. Параметры в методе DecodeJwtToken должны соответствовать TokenValidationParameters в файле Startup.cs. Я также включил несколько интеграционных тестов для SecurityTokenExpiredException, SecurityTokenInvalidSignatureException и SecurityTokenException, чтобы мы могли лучше понять проверку токена.

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

Обновить управление токенами

Поскольку мы применяем технику ротации токенов обновления, серверу может потребоваться сохранить множество токенов обновления и их метаданные. В моем демонстрационном решении я реализовал фоновое задание (код здесь не показан), которое запускается каждую минуту для удаления просроченных токенов обновления в памяти. Вы можете посетить мой репозиторий GitHub (ссылка) для получения более подробной информации.

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

Выдача себя за другое лицо

Иногда мы хотим выдать себя за конкретного пользователя в целях тестирования или отладки. В моем демонстрационном решении я реализовал API impersonation и stop-impersonation. Основная уловка - отслеживать заявки первоначального пользователя. Конечно, требуется некоторая внешняя работа, чтобы сделать процесс олицетворения полностью функциональным.

Интеграционное тестирование

Интеграционное тестирование в ASP.NET Core несложно. Сначала мы создаем Test Host, затем у нас под рукой будут Test HttpClient и ServiceProvider. Таким образом, мы можем добавить токен-носитель в заголовки HTTP и отправлять HTTP-запросы к нашим конечным точкам API. Наконец, мы можем проверить коды ответов и содержимое.

Существует несколько интеграционных тестов, связанных с истечением времени, например, истечение срока действия токена. Прелесть этих тестов в том, что им не нужно заимствовать какое-либо фиктивное время, благодаря параметру DateTime.Now в методах, связанных с токенами, в классе JwtAuthManager. Я рекомендую вам прочитать другую мою статью DateTime.Now Should Be a Parameter.

HTTPS и докеризация

Как упоминалось выше, токены JWT должны передаваться по HTTPS. В режиме разработки мы должны были настроить SSL-сертификат разработчика для localhost с помощью ASP.NET Core, чтобы мы могли запускать приложение с использованием адреса HTTPS.

Когда мы докерируем веб-приложение ASP.NET Core, нам нужно сопоставить закрытый ключ нашего сертификата с контейнером Docker. Вы можете прочитать больше из моей другой статьи Размещение приложения ASP.NET Core на Docker с HTTPS, чтобы узнать больше. В моем репозитории GitHub я включил файл pfx, и мы сможем легко запустить приложение через Docker Compose.

Заключение

Это все на сегодня. Опять же, полное решение находится в моем репозитории GitHub. Надеюсь, это поможет. Поделитесь своими мыслями в разделе комментариев или в моем репозитории GitHub. Если вас интересует соответствующее приложение Angular, вы можете прочитать мою следующую статью: Аутентификация JWT в Angular.

Спасибо за прочтение.