Введение в авторизацию в ASP.NET Core
Это следующая статья из серии статей об аутентификации и авторизации в ASP.NET Core. В первом посте мы представили аутентификацию в ASP.NET Core на высоком уровне, представив концепцию аутентификации на основе утверждений. В следующих двух постах мы более подробно рассмотрели реализации промежуточного программного обеспечения Cookie и JWT , чтобы лучше понять процесс аутентификации. Наконец, мы рассмотрели использование OAuth 2.0 и OpenID Connect в ваших приложениях ASP.NET Core.
В этом посте мы узнаем об аспекте авторизации в ASP.NET Core.
Введение в авторизацию
Напомним, что авторизация — это процесс определения того, имеет ли данный пользователь необходимые атрибуты/разрешения для доступа к данному ресурсу/разделу кода. В ASP.NET Core пользователь определяется ClaimsPrincipal
объектом, который может иметь один или несколько связанных ClaimsIdentity
, которые, в свою очередь, могут иметь любое количество Claims
. Процесс создания ClaimsPrincipal
и присвоения ему правильного Claims
значения является процессом аутентификации. Аутентификация независима и отличается от авторизации, но должна произойти до того, как авторизация может иметь место.
В ASP.NET Core авторизация может предоставляться на основе ряда различных факторов. Они могут быть основаны на ролях текущего пользователя (что было распространено в предыдущей версии .NET), утверждениях текущего пользователя, свойствах ресурса, к которому осуществляется доступ, или любых других свойствах, о которых вам следует подумать. В этом посте мы рассмотрим некоторые из наиболее распространенных подходов к авторизации пользователей в вашем приложении MVC.
Авторизация в MVC
Авторизация в MVC сосредоточена вокруг AuthorizeAttribute
. В простейшей форме применение его к действию (или контроллеру, или глобально) помечает это действие как требующее аутентифицированного пользователя. Думая с точки зрения ClaimsPrincipal
и ClaimsIdentity
, это означает, что текущий принципал должен содержать , ClaimsIdentity
для которого IsAuthenticated=true
.
Это самый грубый уровень детализации — либо вы аутентифицированы, и у вас есть доступ к ресурсу, либо нет, и у вас его нет.
Вы можете использовать the AllowAnonymousAttribute
для игнорирования AuthorizeAttribute
, поэтому в следующем примере только авторизованные пользователи могут вызывать Manage
метод, в то время как любой может вызвать Logout
метод:
[Authorize]
public class AccountController: Controller
{
public IActionResult Manage()
{
return View();
}
[AllowAnonymous]
public IActionResult Logout()
{
return View();
}
}
Под капотом
Прежде чем мы пойдем дальше, я хотел бы уделить минутку тому, чтобы разобраться в том, что на самом деле происходит здесь под одеялом.
Применяется AuthorizeAttribute
к вашим действиям и контроллерам в основном просто атрибут маркера, он не содержит никакого поведения. Вместо этого это то, AuthorizeFilter
что MVC добавляет в свой конвейер фильтрации, когда AuthorizeAttribute
обнаруживает применение к действию. Этот фильтр реализует IAsyncAuthorizationFilter
, так что он вызывается в начале конвейера MVC для проверки авторизации запроса:
public interface IAsyncAuthorizationFilter : IFilterMetadata
{
Task OnAuthorizationAsync(AuthorizationFilterContext context);
}
AuthorizeFilter.OnAuthorizationAsync
вызывается для авторизации запроса, который выполняет ряд действий. Этот метод воспроизведен ниже, для краткости удалены некоторые проверки предварительных условий — мы рассмотрим его через минуту:
public virtual async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var effectivePolicy = Policy;
if (effectivePolicy == null)
{
effectivePolicy = await AuthorizationPolicy.CombineAsync(PolicyProvider, AuthorizeData);
}
if (effectivePolicy == null)
{
return;
}
// Build a ClaimsPrincipal with the Policy's required authentication types
if (effectivePolicy.AuthenticationSchemes != null && effectivePolicy.AuthenticationSchemes.Count > 0)
{
ClaimsPrincipal newPrincipal = null;
for (var i = 0; i < effectivePolicy.AuthenticationSchemes.Count; i++)
{
var scheme = effectivePolicy.AuthenticationSchemes[i];
var result = await context.HttpContext.Authentication.AuthenticateAsync(scheme);
if (result != null)
{
newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result);
}
}
// If all schemes failed authentication, provide a default identity anyways
if (newPrincipal == null)
{
newPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
}
context.HttpContext.User = newPrincipal;
}
// Allow Anonymous skips all authorization
if (context.Filters.Any(item => item is IAllowAnonymousFilter))
{
return;
}
var httpContext = context.HttpContext;
var authService = httpContext.RequestServices.GetRequiredService<IAuthorizationService>();
// Note: Default Anonymous User is new ClaimsPrincipal(new ClaimsIdentity())
if (!await authService.AuthorizeAsync(httpContext.User, context, effectivePolicy))
{
context.Result = new ChallengeResult(effectivePolicy.AuthenticationSchemes.ToArray());
}
}
Во-первых, он вычисляет применимый AuthorizationPolicy
для запроса. Это устанавливает требования, которые должны быть выполнены для авторизации запроса. Следующим шагом является попытка аутентификации запроса путем обращения AuthenticateAsync(scheme)
к AuthenticationManager
найденному адресу HttpContext.Authentication
. Это будет проходить через процесс аутентификации, который я обсуждал в предыдущих сообщениях , и в случае успеха возвращает аутентифицированный ClaimsPrincipal
обратно в фильтр.
После получения аутентифицированного принципала можно начинать процесс авторизации. Во-первых, метод проверяется на предмет IAllowAnonymousFilter
применения (добавляется при AllowAnonymousAttribute
использовании), и если да, то успешно возвращается без какой-либо дальнейшей обработки.
Если требуется авторизация, то фильтр запрашивает экземпляр из IAuthorizationService
файла HttpContext
. Этот сервис аккуратно инкапсулирует всю логику для принятия решения о том, соответствует ли a ClaimsPrincipal
требованиям конкретного AuthorizationPolicy
. Вызов IAuthorizationService.AuthorizeAsync()
возвращает логическое значение, указывающее, был ли результат успешным.
Если IAuthorizationService
указывает, что пользователь не был успешным, AuthorizationFilter
возвращает ChallengeResult
, минуя оставшуюся часть конвейера MVC. При выполнении этот результат вызывает ChallengeAsync
, AuthenticationManager
который, в свою очередь, вызывает HandleUnauthorizedAsync
или HandleForbiddenAsync
базовый объект AuthenticationHandler
, как описано ранее .
Конечным результатом будет либо ошибка 403, указывающая, что у пользователя нет разрешения, либо ошибка 401, указывающая, что пользователь не вошел в систему, что обычно фиксируется и преобразуется в перенаправление на страницу входа.
Детали того, как это AuthorizationFilter
работает, довольно второстепенны по отношению к этому введению в целом, но они подчеркивают разделение проблем и абстракций, используемых для облегчения тестирования, а также использование атрибутов тупых маркеров для работы с другими более сложными сервисами.
Авторизация на основании претензий
Теперь, когда обходной путь завершен, и мы больше понимаем, как работает авторизация в MVC, мы можем рассмотреть создание некоторых конкретных требований авторизации, а не просто «вы вошли в систему».
Как я уже говорил во введении к аутентификации , идентификация в ASP.NET Core на самом деле полностью сосредоточена вокруг Claims
. Учитывая этот факт, одним из наиболее очевидных способов аутентификации является проверка наличия у пользователя данного утверждения. Например, на вашем сайте может быть раздел, доступный только для VIP-персон. Для авторизации запросов вы можете создать CanAccessVIPArea
политику или, точнее, файл AuthorizationPolicy
.
Чтобы создать новую политику, мы настраиваем их как часть конфигурации службы в ConfigureServices
методе вашего Startup
класса с помощью файла AuthorizationPolicyBuilder
. Мы указываем имя для политики "CanAccessVIPArea"
и добавляем требование, чтобы у пользователя было VIPNumber
утверждение:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy(
"CanAccessVIPArea",
policyBuilder => policyBuilder.RequireClaim("VIPNumber"));
});
}
Это требование гарантирует только наличие ClaimsPrincipal
претензии VIPNumber
, оно не предъявляет никаких требований к стоимости претензии. Если нам требуется, чтобы утверждение имело определенные значения, мы можем передать их вRequireClaimMethod:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy(
"CanAccessVIPArea",
policyBuilder => policyBuilder.RequireClaim("VIPNumber", "1", "2"));
});
}
Настроив нашу политику, мы теперь можем применить ее к нашим действиям или контроллерам, чтобы защитить их от пролетариата:
[Authorize(Policy = "CanAccessVIPArea")]
public class ImportantController: Controller
{
public IActionResult FancyMethod()
{
return View();
}
}
Обратите внимание, что если AuthorizeAttribute
к действию применено несколько s, то все политики будут удовлетворены для авторизации запроса.
Авторизация на основе ролей
До того, как была принята проверка подлинности на основе утверждений, распространенным подходом была авторизация по ролям. Как показано ранее , ClaimsPrincipal
все еще есть IsInRole(string role)
метод, который можно использовать при необходимости. В частности, вы можете указать требуемые роли в AuthorizeAttribute
s, которые затем будут проверять, что пользователь находится в правильной роли, прежде чем авторизовать пользователя:
[Authorize(Roles = "HRManager, CEO")]
public class AccountController: Controller
{
public IActionResult ViewUsers()
{
return View();
}
}
Однако, кроме как для простоты переноса с ASP.NET 4.X, я бы не рекомендовал использовать это Roles
свойство в AuthorizeAttribute
. Вместо этого гораздо лучше использовать ту же AuthorizationPolicy
инфраструктуру, что и для Claim
требований. Это обеспечивает гораздо большую гибкость, чем предыдущий подход, упрощая обновление при изменении политик или, например, при необходимости их динамической загрузки.
Настройка политики на основе ролей во многом аналогична политике для Claims
и позволяет указать несколько ролей; членство в любом из них будет удовлетворять требованиям политики.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy(
"CanViewAllUsers",
policy => policy. RequireRole("HRManager", "CEO"));
});
}
Теперь мы можем обновить предыдущий метод, чтобы использовать нашу новую политику:
[Authorize(Policy = "CanViewAllUsers")]
public class AccountController: Controller
{
public IActionResult ViewUsers()
{
return View();
}
}
Позже, если мы решим использовать подход, основанный на утверждениях, для нашей авторизации, мы можем просто обновить политики по мере необходимости, вместо того, чтобы искать все контроллеры в нашем решении, чтобы найти использование строк магической роли.
За кулисами роли a ClaimsPrincipal
на самом деле представляют собой просто утверждения, созданные с помощью типа ClaimsIdentity.RoleClaimType
. По умолчанию это задается ClaimType.Role
строкой http://schemas.microsoft.com/ws/2008/06/identity/claims/role
. Когда пользователь аутентифицируется, для его ролей добавляются соответствующие утверждения, которые можно найти позже по мере необходимости.
Стоит иметь это в виду, если вам трудно AuthorizeAttributes
не работать. Большинство внешних поставщиков удостоверений будут использовать другой набор утверждений , представляющих роль, имя и т. д., которые не совпадают со значениями, используемыми корпорацией Майкрософт в ClaimType
классе. Как обсуждает Доминик Байер в своем блоге , это может привести к ситуациям, когда утверждения не переводятся, и пользователи могут выглядеть не в заданной роли. Если вы столкнетесь с проблемами, из-за которых ваша авторизация не работает должным образом, я настоятельно рекомендую вам ознакомиться со всеми подробностями в его посте.
Вообще говоря, если у вас нет устаревших требований, я бы рекомендовал не использовать роли — по сути, они являются лишь подмножеством Claims
подхода и обеспечивают ограниченную дополнительную ценность.
Резюме
В этом посте представлено введение в авторизацию в ASP.NET Core MVC с использованием платформы AuthorizeAttribute
. Мы коснулись трех простых способов авторизации пользователей — в зависимости от того, прошли ли они проверку подлинности, по политике и по роли. Мы также ненадолго заглянули в подполье, чтобы увидеть, как AuthorisationFilter
работает вызов в рамках конвейера MVC.
В следующем посте мы подробнее рассмотрим политики и рассмотрим, как можно создавать настраиваемые политики и настраиваемые требования.
Только полноправные пользователи могут оставлять комментарии. Аутентифицируйтесь пожалуйста, используя сервисы.