Некоторое время назад перед нами возникла задача разграничения прав доступа к ресурсам, то есть задача аутентификации и управления аутентификацией. Поскольку архитектура основных проектов представляла нечто похожее на распределенный монолит, мы решили остановиться на Identity Server.
Identity Server
Особенности конфигурации
Применение
Identity Server
Наверное лучший способ понять, что такое Identity Server — это прочитать документацию. Если вкратце, то могу сказать, что IS представляет собой OpenID Connect и OAuth 2.0 фреймворк для ASP.NET Core. Стоит отметить, что потенциал фреймворка достаточно огромный.
Если говорить о способах его применения, то, в целом, можно отметить два основных:
внутри приложения — это способ встраивание фреймворка в основное приложение или ядро системы. Такой способ хорош скоростью работы, а также возможностью комбинирования логики и аутентификации, однако не очень хорош с точки зрения гибкости и приводит к нагромождению кода;
отдельный сервис — это выделение фреймворка в отдельный рабочий процесс, такого как — сервис аутентификации, запросы к которому будут идти по сети. К преимуществам здесь можно отнести гибкость и разделение кода, но расплачиваться приходится скоростью;
Ресурсами, которые защищаются с помощью Identity Server, могут быть хранилища файлов, различные сервисы предоставляющие данные, адаптеры конфигурации и прочее. Чтобы ограничить доступ к ресурсам необходимо сконфигурировать IS. Сама конфигурация может храниться в различных местах, например, конфигурация может храниться в базе данных, взаимодействие с которой может осуществляться через EF Core.
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
const string connectionString = …
services.AddIdentityServer()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
});
Здесь подключается IS через AddIdentityServer(), а методы AddConfigurationStore и AddOperationalStore инициализируют конфигурацию и загружают оперативные данные из БД. Если использовать базу данных, то можно «на лету» менять конфигурацию Identity Server и при этом не нужно будет обновлять сервис.
Так же конфигурацию можно хранить в файле, в таком случае, при запуске приложения, она будет загружена в оперативную память.
services.AddIdentityServer()
.AddInMemoryClients(Clients)
.AddInMemoryIdentityResources(Resources)
.AddInMemoryApiResources(ApiResources)
.AddInMemoryApiScopes(Scopes);
Здесь загружаются Clients, Resources, Scopes, ApiResources, ApiScopes в оперативную память, о них пойдет речь ниже.
Особенности конфигурации
Из прошлых вставок кода можно было заметить конфигурационный набор в виде Clients, Resources, ApiResources, Scopes. Наверное, это основные конфигурации, которые нужно встраивать в проект, но что же они означают? Давайте разбираться
Клиенты
Clients — это клиенты, которые могут подключиться к IS и получить разрешение на использование некоторых ресурсов или областей.
{
"ClientId": "client_id",
"ClientSecrets": [ { "Value": "xxx" } ],
"AccessTokenLifetime": "86400",
"AllowedGrantTypes": [ "client_credentials" ],
"AllowedScopes": [
"openid",
"profile",
]
}
Как и каждый пользователь в современном приложении, в конфигурации клиента есть логин и пароль. В качестве логина здесь выступает «ClientId», а в качестве пароля служит «ClientSecrets» в зашифрованном виде.
Нужно понимать, что клиентами, как правило, являются приложения — это приводит нас к необходимости иметь различные типы аутентификации клиентов, за это отвечает параметр «AllowedGrantTypes» в конфигурации клиента. Рассмотрим основные 5 не гибридных типов, каждый из них предназначен для своего сценария входа.
client credentials — предназначен для коммуникации машины к машине — токен запрашивается непосредственно от имени клиента;
authorization code — предназначен для работы с интерактивными пользователями клиентского приложения;
device flow — предназначен для работы с устройствами без браузера или с ограниченными возможностями ввода, к таким относятся например Apple TV;
implicit —в настоящее время всё больше теряет актуальность, так он был предназначен для собственных приложений и приложений JS, где токен доступа возвращался немедленно без дополнительного шага обмена кода авторизации;
resource Owner Password — используется в случае доверительных отношений с клиентом, например для приложений с высоким уровнем привилегий;
Полученный доступ был бы бесконечным, если бы в недрах Identity Server не было бы ограничений по времени, которое настраивается полем «AccessTokenLifetime». AccessTokenLifetime — это время жизни токена доступа. Помимо токена доступа в некоторых типах авторизации используется «RefreshToken». RefreshToken — токен обновления, который позволяет продлевать доступ, для этого необходимо проставить параметр «AllowOfflineAccess» в true и выполнить запрос вида:
POST /connect/token
client_id=client&
client_secret=secret&
grant_type=refresh_token&
refresh_token=hdh922
Ресурсы
Ресурсы в Identity Server 4 разделяются на два вида:
Identity Resources — это ресурсы пользователя такие как: идентификатор пользователя, логин, e-mail и так далее;
API Resources — это функциональные ресурсы, к которым может получить доступ клиент, сюда могут относиться как методы апи, так и очереди сообщений, и прочий функционал;
Так, например, ресурсом может быть хранилище файлов или, например, пространствами (scopes служит для разделения ограничений, но об этом позже) директории внутри хранилища. Также, частый случай, когда ресурсом является целый сервис, а пространствами функциональные части сервиса.
Пространства
Наверное самый простой способ понять, что такое пространства это представить себе минимальный ресурс, например, сервис, который работает с пользователями и реализует операции CRUD (create, read, update, delete) — эти виды операций с сервисом и есть пространства, однако стоит отметить, что существуют и другие варианты адаптации ресурсов и пространств к задаче. Например, пространства могут уточнять определенные ресурсы, к которым запрашивается доступ. В частности, мы можем запросить доступ не ко всем пользователям, а к определенной группе, которая будет указана в пространстве.
Профиль сервис
Профиль сервис предназначен для расширения возможностей по доступу к идентификационным данным пользователей. С его помощью можно идентифицировать, валидировать пользователей, а также влиять на доступ, ограничивать его при необходимости, добавлять свои claims в токен и многое другое (документация). Стоит отметить, что пользователями могут быть как реальные пользователи системы, так и просто части или модули приложения. Сервис должен реализовать интерфейс IProfileService и добавить в конвейер:
services.AddIdentityServer()
.AddProfileService<ProfileService>();
Сервис валидации
Помимо ProfileService можно реализовать свой сервис валидации, который будет проверять как токен доступа, так и весь запрос. Валидация производится после выполнения запроса через основную логику аутентификации и перед отправкой ответа клиенту. С её помощью, можно менять токен доступа, влиять на сведения в этом токене или ограничивать доступ к определенным сущностям (подробнее). Сервис должен реализовать интерфейс ICustomTokenRequestValidator и встроить его в конвейер:
services.AddIdentityServer()
.AddCustomTokenRequestValidator<ClientTokenValidatorService>();
Применение
Итак, Identity Server мы сконфигурировали, а также приняли решение держать его отдельно как самостоятельный сервис, который выдает токены доступа на определенные ресурсы при правильных параметрах запроса. Теперь необходимо заставить сервисы или ресурсы проверять эти токены. И здесь также есть два пути:
Первый и самый очевидный — это проверять токен доступа на самом ресурсе или сервисе, этот способ самый гибкий и самый быстрый. На рисунке у каждого ресурса есть слой, который проверяет токен и управляет доступом к ресурсу. В такой реализации можно применять индивидуальные настройки для каждого ресурса. Например, валидация токена для файлового хранилища может быть сконфигурирована так:
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuerSigningKey = true,
ValidateLifetime = false,
IssuerSigningKey = securityKey,
ValidateIssuer = false,
ClockSkew = TimeSpan.FromMinutes(5)
};
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.TokenValidationParameters = tokenValidationParameters;
});
здесь используется схема аутентификации Bearer, а валидируется только цифровая подпись и учитывается поправка на время функционирования токена (актуально если ресурс и IS на разных машинах, где отличается время). В свою очередь некоторый открытый сервис работы с юзерами может валидировать токен по собственными внутренним правилам.
Обратной стороной здесь является тот факт, что каждый сервис должен уметь читать токен, а значит и иметь ключи доступа на чтение и в случае, когда их нужно будет сменить, придется переопубликовать все ресурсы, зависящие от ключей.
Здесь появляется отдельный сервис, проверки токена и управления аутентификацией. Есть также варианты, когда сервис проверки токена встроен с Identity Server.
Конфигурация валидации и чтения токена доступа будет храниться в одном месте. Этот подход может стать лучшей альтернативой, поскольку он решает все минусы предыдущего, но здесь появляется взаимодействие по сети, балансировка нагрузки и прочие особенности микросервисного подхода, что в целом добавляет время проверки токена и, к слову, каждый ресурс должен знать о том к кому обращаться, что приводит нас к единому хранилищу конфигураций, впрочем это уже другая история.
Комментарии (9)
rus_sus
09.07.2024 09:15+2задача разграничения прав доступа к ресурсам, то есть задача аутентификации
только это задача авторизации
VanGoghDev
09.07.2024 09:15Большое спасибо автору за статью! У меня есть запрос на рассказ про "другую историю". Интересно узнать как настраивается валидация токенов на IS и как клиентское приложение конфигурируется при таком workflow.
alex_smite Автор
09.07.2024 09:15Валидация токенов осуществляется в каждом сервисе отдельно, IS мы используем только для выдачи полномочий. То есть шифровать или записывать токен может только IS, а читать каждый сервис самостоятельно:
var tokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, ValidateIssuerSigningKey = true, ValidateLifetime = false, IssuerSigningKey = securityKey, ValidateIssuer = false, }; services.AddSingleton(tokenValidationParameters) .AddSingleton<JwtSecutiryTokenDecodeService>() .AddSingleton<AccessVerificator>(); services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.TokenValidationParameters = tokenValidationParameters; }); services.AddAuthorization(options => { // политика по-умолчанию общая для всех options.AddPolicy("TestPolicy", policy => { policy.AuthenticationSchemes = new List<string> { "Bearer" }; policy.RequireAuthenticatedUser(); policy.RequireClaim("aud", "test"); }); });
Free_ze
Статье 2 часа, а ссылки на документацию уже успели побиться. Есть подозрение, что в черновиках она пробыла несколько лет.
Тут главное:
Может пора закопать стюардессу?
remzalp
Тут скорей особенность в том, что в этой версии лицензия еще позволяет где попало использовать
Free_ze
Для "где попало" актуальная версия и так будет бесплатной, а для компании с доходом $1M+ это обойдется в сумму с десяток тысяч долларов в год. Не факт, что поддерживать на своих плечах легаси выйдет дешевле.
alex_smite Автор
Ссылки перебил, по поводу "закопать стюардессу". Для относительно простых задач авторизации, в частности, доступ к полному списку сущностей или к конкретной сущности IS используем до сих пор и, как по мне, он прекрасно справляется, ну, для более сложных ACL, ABAC или RBAC (https://habr.com/ru/companies/custis/articles/248649/)
Free_ze
А я вам и не предлагаю отказываться от IS. Речь про версию 4, которая уже не поддерживается, в то время, как есть актуальная и живая 7.
alex_smite Автор
Спасибо за предложение, обязательно попробуем!