Создание ограничений для авторизации

Последнее обновление: 12.01.2022

Хотя встроенный функционал по созданию политик авторизации покрывает множество случаев для их определения, но он имеет ограниченные возможности. Например, пусть у нас есть класс Person, где свойство Year хранит год рождения пользователя:

record class Person(string Email, string Password, int Year);

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

using Microsoft.AspNetCore.Authorization;

class AgeRequirement : IAuthorizationRequirement
{
    protected internal int Age { get; set; }
	public AgeRequirement(int age) => Age = age;
}

Класс ограничения должен реализовать интерфейс IAuthorizationRequirement из постранства имен Microsoft.AspNetCore.Authorization. С помощью свойства Age устанавливается минимально допустимый возраст.

Сам класс ограничения только устанавливает некоторые лимиты, больше он ничего не делает. Чтобы его использовать при обработке запроса, нам надо добавить специальный класс - обработчик. Итак, добавим в проект еще один новый класс, который назовем AgeHandler:

using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;

class AgeHandler : AuthorizationHandler<AgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        AgeRequirement requirement)
    {
        // получаем claim с типом ClaimTypes.DateOfBirth - год рождения
        var yearClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);
        if (yearClaim is not null)
        {
            // если claim года рождения хранит число
            if (int.TryParse(yearClaim.Value, out var year))
            {
                // и разница между текущим годом и годом рождения больше требуемого возраста
                if ((DateTime.Now.Year - year) >= requirement.Age)
                {
                    context.Succeed(requirement); // сигнализируем, что claim соответствует ограничению
                }
            }
        }
        return Task.CompletedTask;
    }
}

Класс обработчика должен наследоваться от класса AuthorizationHandler<T>, где параметр T представляет тип ограничения. Вся обработка производится в методе HandleRequirementAsync(). Этот метод вызывается системой авторизации при доступе к ресурсу, к которому применяется ограничение, используемое обработчиком.

В качестве параметов метод HandleRequirementAsync() получает объект применяемого ограничения и контекст авторизации AuthorizationHandlerContext, который содержит информацию о запросе. В частности, через свойство User он возвращает объект ClaimPrincipal, представляющий текущего пользователя.

А методы класса AuthorizationHandlerContext позволяют управлять авторизацией. Так, метод Succeed(requirement) вызывается, если запрос соответствует ограничению requirement.

И наоброт, метод Fail(), если запрос не соответствует ограничению.

В данном случае мы получаем для текущего пользователя claim с типом ClaimTypes.DateOfBirth. Предполагается, что этот claim содержит год рождения пользователя. И далее по этому году получаем возраст пользователя относительно текущей даты. И если возраст оказался больше минимально допустимого, то вызываем метод context.Succeed(requirement). Вызов этого метода будет означать, что работа обработчика завершилась успешно. Если этот метод не вызывается, то считается, что авторизация прошла неудачно.

Определим в файле Program.cs следующий код:

using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;

var people = new List<Person> 
{ 
    new Person("tom@gmail.com", "12345", 1984),
    new Person("bob@gmail.com", "55555", 2006)
};

var builder = WebApplication.CreateBuilder();
// встраиваем сервис AgeHandler
builder.Services.AddTransient<IAuthorizationHandler, AgeHandler>();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/login";
        options.AccessDeniedPath = "/login";
    });
builder.Services.AddAuthorization(opts => {
    // устанавливаем ограничение по возрасту
    opts.AddPolicy("AgeLimit", policy => policy.Requirements.Add(new AgeRequirement(18)));
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/login", async (HttpContext context) =>
{
    context.Response.ContentType = "text/html; charset=utf-8";
    // html-форма для ввода логина/пароля
    string loginForm = @"<!DOCTYPE html>
    <html>
    <head>
        <meta charset='utf-8' />
        <title>METANIT.COM</title>
    </head>
    <body>
        <h2>Login Form</h2>
        <form method='post'>
            <p>
                <label>Email</label><br />
                <input name='email' />
            </p>
            <p>
                <label>Password</label><br />
                <input type='password' name='password' />
            </p>
            <input type='submit' value='Login' />
        </form>
    </body>
    </html>";
await context.Response.WriteAsync(loginForm);
});

app.MapPost("/login", async (string? returnUrl, HttpContext context) =>
{
    // получаем из формы email и пароль
    var form = context.Request.Form;
    // если email и/или пароль не установлены, посылаем статусный код ошибки 400
    if (!form.ContainsKey("email") || !form.ContainsKey("password"))
        return Results.BadRequest("Email и/или пароль не установлены");
    string email = form["email"];
    string password = form["password"];

    // находим пользователя 
    Person? person = people.FirstOrDefault(p => p.Email == email && p.Password == password);
    // если пользователь не найден, отправляем статусный код 401
    if (person is null) return Results.Unauthorized();
    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, person.Email),
        new Claim(ClaimTypes.DateOfBirth, person.Year.ToString())
    };
    var claimsIdentity = new ClaimsIdentity(claims, "Cookies");
    var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
    await context.SignInAsync(claimsPrincipal);
    return Results.Redirect(returnUrl ?? "/");
});
// доступ только для тех, кто соответствует ограничению AgeLimit
app.Map("/age", [Authorize(Policy = "AgeLimit")]() => "Age Limit is passed");

app.Map("/", [Authorize](HttpContext context) =>
{
    var login = context.User.FindFirst(ClaimTypes.Name);
    var year = context.User.FindFirst(ClaimTypes.DateOfBirth);
    return $"Name: {login?.Value}\nYear: {year?.Value}";
});
app.MapGet("/logout", async (HttpContext context) =>
{
    await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    return "Данные удалены";
});

app.Run();

Здесь для тестирования механизма авторизации по возрасту определена условная бд - список пользователей people:

var people = new List<Person> 
{ 
    new Person("tom@gmail.com", "12345", 1984),
    new Person("bob@gmail.com", "55555", 2006)
};

Для настройки авторизации в коллекции сервисов необходимо зарегистрировать зависимость для сервиса IAuthorizationHandler:

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

Далее добавляем политику "AgeLimit"? для которой добавляется кастомное ограничение:

builder.Services.AddAuthorization(opts => {
    // устанавливаем ограничение по возрасту
    opts.AddPolicy("AgeLimit", policy => policy.Requirements.Add(new AgeRequirement(18)));
});

Для входа пользователей в приложение определена конечная точка app.MapGet("/login"), которая обрабатывает GET-запросы по пути "/login" и отправляет пользователям форму для ввода логина и пароля.

После заполнения и отправки формы логина данные в POST-запросе получает конечная точка app.MapPost("/login"), которая получает логин и пароль и по них находит пользователя в списке people. Значения год рождения найденного пользователя добавляется в список claims:

var claims = new List<Claim>
{
	new Claim(ClaimTypes.Name, person.Email),
        new Claim(ClaimTypes.DateOfBirth, person.Year.ToString())
};
var claimsIdentity = new ClaimsIdentity(claims, "Cookies");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

И далее мы можем использовать созданную политику для ограничения доступа. Для тестирования политики определена следующая конечная точка:

// доступ только для тех, кто соответствует ограничению AgeLimit
app.Map("/age", [Authorize(Policy = "AgeLimit")]() => "Age Limit is passed");

Таким образом, по адресу "/age" смогут обратиться только те, кто удовлетворяет ограничению AgeLimit (в данном случае кому исполнилось 18 лет).

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850