Введение
Тема аутентификации и авторизации всегда будет актуальна для большинства web-приложений. Многие .NET разработчики уже успели познакомиться с Windows Identity Foundation (WIF), его подходами и возможностями для реализации так называемых «identity-aware» приложений. Для тех, кто не успел поработать с WIF, первое знакомство можно начать с изучения следующего раздела MSDN. В данной же статье я предлагаю более детально взглянуть на так называемый «claims-based» подход к авторизации пользователей путем изучения того, как это может выглядеть на примере.
Claims-Based Authorization
«Claims-Based» авторизация это подход, при котором решение авторизации о предоставлении или запрете доступа определенному пользователю базируется на произвольной логике, которая в качестве входных данных использует некий набор «claims» относящихся к этому пользователю. Проводя аналогию с «Role-Based» подходом, у некого администратора в его наборе «claims» будет только один элемент с типом «Role» и значением «Administrator», например. Более детально, о преимуществах и проблемах, которые решает этот подход можно прочесть на том же MSDN, также советую посмотреть лекцию Доминика Байера.
В целом, вышеупомянутый подход поощряет разработчиков к разделению бизнес логики приложения от логики авторизации и это действительно удобно. Так как же это выглядит на практике? К ней собственно и приступим.
Постановка задачи
Предположим, что нужно создать некий API сервис, который будет доступен нескольким клиентским приложениям. Функционал у клиентских приложений разный, пользователи также. Возможно, появятся еще и другие клиентские приложения, со своими пользователями и схемой взаимодействия с API, поэтому нам необходимо иметь гибкую систему авторизации для того, чтобы иметь возможность на любом этапе сконфигурировать политику доступа к API для того или иного приложения/пользователя. API в нашем случае будет построено с использованием ASP.NET Web API 2.0, клиентскими приложениями будут, например, Windows Phone приложение и Web-сайт.
Рассмотрим приложения их пользователей и функционал более детально:
Windows Phone клиент
- Сам по себе может только регистрировать новых пользователей.
- Зарегистрированные пользователи могут:
- просматривать свой профиль;
- обновлять свой профиль;
- производить смену своего пароля;
Web клиент
- Сам по себе не имеет доступа к API.
- Зарегистрированные мобильным клиентом пользователи могут:
- просматривать свой профиль;
- обновлять свой профиль;
- производить смену своего пароля;
- Администраторы системы могут:
- всё то же, что и пользователи для своего аккаунта;
- всё то же, что и пользователи для аккаунта любого пользователя;
- просматривать список всех зарегистрированный пользователей;
- создавать/удалять пользователей;
Итак, мы имеем представление о том, какой функционал должен предоставляться через API, каким клиентам и с какими правилами. Что ж, приступим к реализации!
Реализация
Начнем с определения интерфейса будущего API сервиса:
public interface IUsersApiController { // List all users. IEnumerable<User> GetAllUsers(); // Lookup single user. User GetUserById(int id); // Create user. HttpResponseMessage Post(RegisterModel user); // Restore user's password. HttpResponseMessage RestorePassword(string email); // Update user. HttpResponseMessage Put(int id, UpdateUserModel value); // Delete user. HttpResponseMessage Delete(string email); }
Непосредственную реализацию API оставим за скобками данной статьи, по крайней мере, для примера сойдет и вариант вроде этого:
public class UsersController : ApiController { //... public HttpResponseMessage Post([FromBody]RegisterModel user) { if (ModelState.IsValid) { return Request.CreateResponse(HttpStatusCode.OK, "Created!"); } else { return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); } } //... }
Следующим шагом создадим наследника класса ClaimsAuthorizationManager и переопределим некоторые его методы. ClaimsAuthorizationManager
— это именно тот компонент WIF, который позволяет в одном месте перехватывать входящие запросы и выполнять произвольную логику, которая исходя из набора «claims» текущего пользователя* решает о предоставлении или запрете доступа.
* — о том, где этот набор формируется поговорим чуть позже.
Не уходя далеко, мы можем позаимствовать его реализацию из MSDN по этой ссылке. Как видим из секции «Examples» переопределены следующие методы:
/// <summary> /// Overloads the base class method to load the custom policies from the config file /// </summary> /// <param name="nodelist">XmlNodeList containing the policy information read from the config file</param> public override void LoadCustomConfiguration(XmlNodeList nodelist) {...} /// <summary> /// Checks if the principal specified in the authorization context is authorized /// to perform action specified in the authorization context on the specified resource /// </summary> /// <param name="pec">Authorization context</param> /// <returns>true if authorized, false otherwise</returns> public override bool CheckAccess(AuthorizationContext pec) {...}
Глядя на реализацию и комментарии к ней, можно разобраться что происходит и я не буду останавливаться на этом. Отмечу только формат политики доступа из этого примера:
... <policy resource="http://localhost:28491/Developers.aspx" action="GET"> <or> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="developer" /> <claim claimType="http://schemas.xmlsoap.org/claims/Group" claimValue="Administrator" /> </or> </policy> <policy resource="http://localhost:28491/Administrators.aspx" action="GET"> <and> <claim claimType="http://schemas.xmlsoap.org/claims/Group" claimValue="Administrator" /> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country" claimValue="USA" /> </and> </policy> <policy resource="http://localhost:28491/Default.aspx" action="GET"> </policy> ...
Политика доступа здесь — это набор секций «policy», каждая из которых идентифицируется такими атрибутами как «resource» и «action». Внутри каждой такой секции перечислены «claims» которые необходимы для доступа к ресурсу. В случае WebApi «resource» — это имя контроллера, «action» — имя action-метода. Более того, есть возможность строить правила доступа с использованием логических условий*.
* — и всё бы замечательно если бы в текущей реализации была возможность конфигурировать больше 2-x элементов «claim» внутри блоков «and» или «or».
Пока используем всё «as-is», за исключением названия наследника, его изменим на XmlBasedAuthorizationManager
. Если попробовать сбилдить проект, то окажется что нам не хватает класса PolicyReader
, его можно взять из полных исходных кодов MSDN-примера.
После того, как новая реализация готова, сконфигурируем WebAPI приложение для использования ее в качестве менеджера авторизации. Для этого:
1. Зарегистрируем конфигурационные секции обязательные для работы WIF:
<configSections> <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" /> <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" /> <!-- Others sections--> </configSections>
2. Укажем какую реализацию следует использовать в качестве менеджера авторизации:
<system.identityModel> <identityConfiguration> <claimsAuthorizationManager type="YourProject.WebApi.Security.XmlBasedAuthorizationManager, YourProject.WebApi, Version=1.0.0.0, Culture=neutral"> <!-- Policies --> </claimsAuthorizationManager> <claimsAuthenticationManager type="YourProject.WebApi.Security.AuthenticationManager, YourProject.WebApi, Version=1.0.0.0, Culture=neutral" /> </identityConfiguration> </system.identityModel>
Отлично, мы указали WIF какую реализацию использовать, но как вы заметили, в конфигурации выше остались две детали:
- вместо набора xml-секций «policy» у нас пусто;
- присутствует xml-элемент "
claimsAuthenticationManager
", о котором я не упоминал ранее.
Рассмотрим эти пункты по порядку.
1. Конфигурация политики доступа
Возвращаясь к постановке задачи, а также учитывая уже рассмотренный формат политики доступа, попробуем составить конфигурацию для нашего API. Получится следующий набор правил:
<policy resource="Users" action="GetAllUsers"> <and> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WebApplication" /> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="Admin" /> </and> </policy> <policy resource="Users" action="Post"> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WPhoneApplication" /> </policy> <policy resource="Users" action="RestorePassword"> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WPhoneApplication" /> </policy> <policy resource="Users" action="GetUserById"> <or> <and> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WebApplication" /> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="Admin" /> </and> <and> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="User" /> <!-- Как указать вместо {0} идентификатор пользователя который отправил "request" ? --> <!-- <claim claimType="UserId" claimValue="{0}" /> --> </and> </or> </policy> <policy resource="Users" action="Put"> <or> <and> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WebApplication" /> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="Admin" /> </and> <and> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="User" /> <!-- Как указать вместо {0} идентификатор пользователя который отправил "request" ? --> <!-- <claim claimType="UserId" claimValue="{0}" /> --> </and> </or> </policy>
Видим, что некоторые policy-секции проще, некоторые сложнее, некоторые повторяются. Рассмотрим по частям, начиная с простого варианта — политика доступа для получения списка пользователей:
<policy resource="Users" action="GetAllUsers"> <and> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WebApplication" /> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="Admin" /> </and> </policy>
Все предельно очевидно: доступ к данному ресурсу есть у тех пользователей, набор «claims» которых содержит оба «сlaim» — элемента.
Теперь более сложный вариант — получение информации о пользователе по идентификатору:
<policy resource="Users" action="GetUserById"> <or> <and> <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system" claimValue="WebApplication" /> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="Admin" /> </and> <and> <claim claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" claimValue="User" /> <!-- Как указать вместо {0} идентификатор пользователя который отправил "request" ? --> <!-- <claim claimType="UserId" claimValue="{0}" /> --> </and> </or> </policy>
Возвращаясь к требованиям, к данному ресурсу могут иметь доступ только администраторы веб приложения, а также пользователи при условии, что каждый пользователь может получать данные только по своему аккаунту. Как видим, первое требование мы без труда устанавливаем в первом <and>..</and>
блоке. Но как же быть с пользователями?
К сожалению, текущая реализация, которую Мы доблестно скопировали, не позволяет сейчас конфигурировать это условие. К тому же, как я уже упоминал выше, она также не позволяет использовать внутри логических "and/or
" блоков вложенные элементы. Если уж быть предельно честным, то эта реализация жестко устанавливает количество «claim» элементов равное двум внутри "and/or
" блоков.
Что касается условия «каждый отдельный пользователь может получать данные только по своему аккаунту», то я планирую предложить свой вариант решения в следующей статье. Предлагаю пока смириться с тем, что все пользователи могут просматривать информацию друг о друге, как выходит из составленной конфигурации. Особенно пока реализация метода GetUserById
выглядит как throw new NotImplementedException()
.
А вот чтобы текущая конфигурация работала исправно мы немного изменим реализацию класса PolicyReader
:
/// <summary> /// Read the Or Node /// </summary> /// <param name="rdr">XmlDictionaryReader of the policy Xml</param> /// <param name="subject">ClaimsPrincipal subject</param> /// <returns>A LINQ expression created from the Or node</returns> private Expression<Func<ClaimsPrincipal, bool>> ReadOr(XmlDictionaryReader rdr, ParameterExpression subject) { Expression defaultExpr = Expression.Invoke((Expression<Func<bool>>)(() => false)); while (rdr.Read()) { if (rdr.NodeType != XmlNodeType.EndElement && rdr.Name != "or") { defaultExpr = Expression.OrElse(defaultExpr, Expression.Invoke(ReadNode(rdr, subject), subject)); } else break; } rdr.ReadEndElement(); Expression<Func<ClaimsPrincipal, bool>> resultExpr = Expression.Lambda<Func<ClaimsPrincipal, bool>>(defaultExpr, subject); return resultExpr; } /// <summary> /// Read the And Node /// </summary> /// <param name="rdr">XmlDictionaryReader of the policy Xml</param> /// <param name="subject">ClaimsPrincipal subject</param> /// <returns>A LINQ expression created from the And node</returns> private Expression<Func<ClaimsPrincipal, bool>> ReadAnd(XmlDictionaryReader rdr, ParameterExpression subject) { Expression defaultExpr = Expression.Invoke((Expression<Func<bool>>)(() => true)); while (rdr.Read()) { if (rdr.NodeType != XmlNodeType.EndElement && rdr.Name != "and") { defaultExpr = Expression.AndAlso(defaultExpr, Expression.Invoke(ReadNode(rdr, subject), subject)); } else break; } rdr.ReadEndElement(); Expression<Func<ClaimsPrincipal, bool>> resultExpr = Expression.Lambda<Func<ClaimsPrincipal, bool>>(defaultExpr, subject); return resultExpr; }
Что ж, мы сконфигурировали политику доступа к ресурсам нашего API, создали реализацию менеджера авторизации, который умеет работать с нашей конфигурацией. Теперь можно перейти к аутентификации — этапу, который предшествует авторизации.
2. Аутентификация и ClaimsAuthenticationManager
Еще до того как принимать решение имеет ли пользователь доступ к ресурсу, сперва нужно произвести аутентификацию, и если она успешна — наполнить набор «claims» пользователя.
Для аутентификации будем использовать Basic Authentication и, например, ее реализацию в Thinktecture.IdentityModel.45. Для этого в NuGet-консоли выполним команду:
Install-Package Thinktecture.IdentityModel
Код класса WebApiConfig
изменим, чтобы он был приблизительно следующим:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { var authentication = CreateAuthenticationConfiguration(); config.MessageHandlers.Add(new AuthenticationHandler(authentication)); config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); config.EnableSystemDiagnosticsTracing(); config.Filters.Add(new ClaimsAuthorizeAttribute()); } private static AuthenticationConfiguration CreateAuthenticationConfiguration() { var authentication = new AuthenticationConfiguration { ClaimsAuthenticationManager = new AuthenticationManager(), RequireSsl = false //only for testing }; #region Basic Authentication authentication.AddBasicAuthentication((username, password) => { var webSecurityService = ServiceLocator.Current.GetInstance<IWebSecurityService>(); return webSecurityService.Login(username, password); }); #endregion return authentication; } }
Здесь отмечу только то, что для проверки credentials пришедших из запроса у меня используется некий IWebSecurityService
. Вы можете использовать здесь свою логику, например: return username == password;
Теперь при каждом запросе к любому ресурсу будет производиться проверка аутентификации, но еще нам нужно трансформировать базовый набор «claims» текущего пользователя. Этим занимается ClaimsAuthenticationManager
, а точнее наш наследник этого класса, который мы уже зарегистрировали:
public class AuthenticationManager : ClaimsAuthenticationManager { public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal) { if (!incomingPrincipal.Identity.IsAuthenticated) { return base.Authenticate(resourceName, incomingPrincipal); } var claimsService = ServiceLocator.Current.GetInstance<IUsersClaimsService>(); var claims = claimsService.GetUserClaims(incomingPrincipal.Identity.Name); foreach (var userClaim in claims) { incomingPrincipal.Identities.First().AddClaim(new Claim(userClaim.Type, userClaim.Value)); } return incomingPrincipal; } }
Как видим, если пользователь прошел аутентификацию — происходит получение его набора «claims», скажем из БД, посредством использования вновь созданного экземпляра IUsersClaimsService
. После «трансформации» экземпляр ClaimsPrincipal возвращается дальше в конвеер для последующего использования, например, авторизацией.
Проверка результата
Пришло время проверить работоспособность нашего решения. Для этого нам естественно понадобятся пользователи с теми или иными «claims». Не будем долго фантазировать над тем откуда их взять и немного видоизменим AuthenticationManager
в целях тестирования. Вместо использования IUsersClaimsService
вставим следующий код:
public class AuthenticationManager : ClaimsAuthenticationManager { public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal) { ... if (incomingPrincipal.Identity.Name.ToLower().Contains("user")) { incomingPrincipal.Identities.First().AddClaim(new Claim(ClaimTypes.Role, "User")); } return incomingPrincipal; } }
Отлично, теперь все пользователи, логин которых содержит слово «user» будут содержать нужный «claim».
Запустим проект и перейдем по ссылке localhost:[port]/api/users
Вводим заветные логин и пароль, наша незамысловатая авторизация проверить их на равенство, а менеджер авторизации трансформирует набор «claims»:
Продолжим выполнение и убедимся, что простой смертный не может просматривать список всех пользователей:
Теперь давайте вспомним о том, что на этапе конфигурирования политики доступа нам пришлось на некоторое время разрешить всем пользователям просматривать информацию друг о друге, этим и воспользуемся. Попробуем узнать о пользователе с Id=100, зайдя по ссылке ~/api/users/100:
И вот мы наблюдаем, что некая реализация, появившаяся в кулуарах, возвращает информацию о любом пользователе 🙂
Заключение
Итак мы познакомились с некоторыми возможностями WIF, разобрали пример того, с чего можно начать при построении гибкой системы авторизации, а также немного «покодировали».
Спасибо за внимание.
ссылка на оригинал статьи http://habrahabr.ru/post/198424/