Хотя встроенный функционал по созданию политик авторизации покрывает множество случаев для их определения, но он имеет ограниченные возможности. Например, пусть у нас есть класс 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 лет).