Аутентификация и авторизация в ASP.NET Core MVC. Политики авторизации

Политики авторизации позволяют создавать более гибкую систему авторизации пользователей в ASP.NET Core MVC, позволяя использовать любые доступные утверждения (claims). На данный момент мы изучили возможности авторизации по ролям, а также, что из себя представляют утверждения и как ими управлять. В этой части мы рассмотрим применение политик авторизации.

Политика авторизации

Политика авторизации — это набор правил, которым должен соответствовать пользователь, чтобы получить доступ к ресурсу. При этом, в политику авторизации можно включать такие требования, как соответствие пользователя определенной роли.

Политики авторизации создаются в классе Program. В предыдущей части мы добавили для пользователя следующее утверждение:

Создадим следующую политику авторизации:

  1. Пользователь должен принадлежать к роли ADMIN
  2. У пользователя должно быть создано утверждение, представленное выше, то есть «Language = Russian».

Изменим класс Program следующим образом:

//настраиваем политику авторизации
builder.Services.AddAuthorization(options => 
{
    options.AddPolicy("OnlyRussianAdmin", policy => 
    {
        policy.RequireRole("ADMIN");
        policy.RequireClaim("Language", "Russian");
    });
});

Все настройки политик авторизации в ASP.NET Core осуществляются при добавлении в проект сервиса авторизации методом AddAuthorization().  В нашем примере options представляет собой объект типа AuthorizationOptions с помощью которого мы управляем политиками авторизации, используя следующие свойства и методы:

Свойство DefaultPolicy Возвращает или задает политику авторизации по умолчанию, которая будет использоваться, если атрибут Authorize задается без параметров
Метод AddPolicy() Добавляет новую политику с указанным именем, созданную на основе делегата .
Метод GetPolicy() Возвращает политику для указанного имени или значение NULL, если политика с этим именем не существует.

В нашем примере, мы добавили новую политику методом AddPolicy() у которого первый параметр метода — имя новой политики авторизации, а второй — делегат, в котором мы, используя AuthorizationPolicyBuilder настраиваем политику. 

Класс AuthorizationPolicyBuilder содержит довольно много свойств и методов, позволяющих настраивать и объединять различные политики авторизации. Приведем основные из них:

Requirements Возвращает или задает список требований, которые должны выполняться чтобы пользователь был авторизован
AddRequirements() Добавляет новое требование в список Requirements.
RequireAuthenticatedUser() Пользователь должен быть аутентифицирован, чтобы чтобы соответствовать политике
RequireClaim(type) У пользователя должно быть определенное утверждение (Claim), чтобы соответствовать политике. При этом, значение Claim не проверяется — только наличие
RequireClaim(type, values) у пользователя должен быть определен Claim с типом type и одним из значений, перечисленных в values
RequireRole(values) у пользователя должны быть определена хотя бы одна роль из списка values
RequireUserName(name) пользователь должен иметь определенное имя, чтобы соответствовать политике

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

Чтобы применить новую политику авторизации мы также используем атрибут Authorize. Например,

[Authorize(Policy = "OnlyRussianAdmin")]
public IActionResult Privacy()
{
    return View();
}

Теперь доступ у действию Privacy() будут иметь только те пользователи, которые соответствую заданной в параметрах атрибута политике. Не менее полезным, при определении политик авторизации, является метод AddRequirements() и связанное с ним свойство Requirements .

Пользовательские требования к авторизации

Пользовательские требования к авторизации удобно применять там, где недостаточно использования только утверждений и ролей. Например, рассмотрим такой момент, как возраст пользователя. Часто (и даже на страницах этого блога), когда речь заходит о политике авторизации, в качестве примера для политики авторизации используется такое требование, как «пользователь должен быть старше/младше заданного возраста». И, далее, объекту пользователя добавляется некое утверждение (Claim) типа Age = 24 и используется в примере. Для примера такой подход вполне работоспособен, однако, в реальном приложении может вызвать серьезные проблемы. Поясним это, опять же, на примере.

Итак, пусть в нашем приложении есть ресурс доступ к которому строго запрещен пользователям младше 18 лет. В приложении регистрируется пользователь, которому на момент регистрации 17 лет и 11 месяцев. Мы устанавливаем объекту такого пользователя claim с его реальным возрастом, например, Age=17.9 и, тем самым, блокируем доступ к ресурсу. Проходит 2 месяца, пользователю уже явно больше 18 лет, а claim так и остался — 17.9 лет, либо нам необходимо будет как-то вручную отслеживать возраста всех пользователей и менять им утверждение Age, что тоже не логично.

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

Продемонстрируем все вышесказанное на примере. Для этого, нам потребуется внести небольшое изменение в процедуру регистрации нового пользователя, а именно, в действие Register() контроллера AccountController:

[Route("{area}/{action}")]
[HttpPost]
public async Task<IActionResult> Register(RegisterModel model)
{
    var user = model.GetUser();
    IdentityResult result = await _userManager.CreateAsync(user, model.Password);
    if (result.Succeeded)
    {
        user = await _userManager.FindByEmailAsync(user.Email);
        //добавляем дату рождения в Claimd польщователя
        await _userManager.AddClaimAsync(user, new Claim("Birthday",model.Birthday.ToString("dd.MM.yyyy"),ClaimValueTypes.String));
        return RedirectToAction("Index", "Home");
    }
    else
    { 
        foreach (var error in result.Errors) 
        {
            ModelState.AddModelError("", error.Description);
        }
        return View();
    }
}

Теперь, при регистрации, пользователю сразу будет добавлено новое утверждение с датой его рождения. Создадим новое требование к авторизации пользователя. Для этого нам необходимо:

  1. Создать класс, реализующий интерфейс IAuthorizationRequirement. Этот класс будет предоставлять механизм проверки того, прошел ли пользователь авторизацию или нет
  2. Создать класс-наследник абстрактного класса AuthorizationHandler, который будет производить необходимую проверку пользователя на соответствие требованию

Добавим в проект новую папку с названием CustomPolicy (папка может иметь любое название) и добавим в эту папку два файла. Первый файл назовем OlderThenPolicy.cs и класс в этой файле будет содержать максимально простой код:

using Microsoft.AspNetCore.Authorization;

namespace AspMvcAuth.CustomPolicy
{
    public class OlderThenPolicy : IAuthorizationRequirement
    {
        public int Age { get; set; }
        public OlderThenPolicy(int age) 
        { 
            Age = age;
        }
    }
}

По сути, это требование к пользователю. Здесь мы указываем минимальный возраст пользователя (18 лет). Второй файл назовем OlderThenHandler.cs и разместим в нем класс-обработчик нашего требования. Код класса будет следующий:

public class OlderThenHandler : AuthorizationHandler<OlderThenPolicy>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OlderThenPolicy requirement)
    {
        var claim = context.User.Claims.Where(claim => claim.Type == "Birthday").FirstOrDefault();
        if (claim != null)
        {
            var date = DateTime.ParseExact(claim.Value, "dd.MM.yyyy", CultureInfo.InvariantCulture);
            var age = DateTime.Now.Year - date.Year;
            if (DateTime.Now.DayOfYear < date.DayOfYear)
                age++;
            if (age>=requirement.Age) 
            {
                context.Succeed(requirement);
            }
            else
                context.Fail();
        }
        else
            context.Fail();
        return Task.CompletedTask;
    }
}

здесь в методе HandleRequirementAsync происходит проверка возраста пользователя. Для этого мы вначале пробуем получить утверждение, содержащее дату рождения пользователя:

var claim = context.User.Claims.Where(claim => claim.Type == "Birthday").FirstOrDefault();

Если у пользователя отсутствует это утверждение, то считаем, что проверка провалена и мы об этом сообщаем, вызвав метод:

context.Fail();

Если же дата рождения получена, то мы вычисляем возраст пользователя с точностью до целого числа лет:

var date = DateTime.ParseExact(claim.Value, "dd.MM.yyyy", CultureInfo.InvariantCulture);
var age = DateTime.Now.Year - date.Year;
if (DateTime.Now.DayOfYear < date.DayOfYear)
  age++;

и сравниваем полученное значение с заданным в требовании:

if (age>=requirement.Age)

если условие соблюдается, то проверка требования прошла успешно, о чем мы сообщаем вызвав метод

context.Succeed(requirement);

Чтобы добавить пользовательское требование к авторизации, изменим настройки сервиса авторизации следующим образом:

builder.Services.AddTransient<IAuthorizationHandler, OlderThenHandler>();
//настраиваем политику авторизации
builder.Services.AddAuthorization(options => 
{
    options.AddPolicy("OnlyRussianAdmin", policy => 
    {
        policy.RequireRole("ADMIN");
        policy.RequireClaim("Language", "Russian");
        policy.AddRequirements(new OlderThenPolicy(18)); //пользователь старше 18 лет
    });
});

Для того, чтобы наше требование проверялось, мы зарегистрировали обработчик OlderThenHandler в качестве сервиса:

builder.Services.AddTransient<IAuthorizationHandler, OlderThenHandler>();

Так как мы уже применили эту политику авторизации в приложении, то можно проверить её действие с учётом нового требования. На данный момент у нас уже есть в базе один пользователь БЕЗ указания в Claims его даты рождения. Попробуем получить доступ к защищенному ресурсу:

Теперь зарегистрируем нового пользователя с датой рождения, например, 01.01.1990:

Войдем в приложение с новым логином/паролем

Добавим пользователя в группу ADMIN:

Добавим пользователь утверждение Language:

Теперь пользователь Alice полностью соответствует политике авторизации, а именно:

  1. Входит в роль ADMIN
  2. Содержит утверждение Language = Russian
  3. Имеет возраст старше 18 лет

Получим доступ к защищенному ресурсу:

Доступ получен, следовательно, политика авторизации работает.

Скачать код проекта из Github

Итого

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

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии