Атрибут Authorize легко позволяет разграничить доступ в зависимости от роли, однако для создания авторизации
функциональности ролей бывает недостаточно. Например, что если мы хотим разграничить доступ на основе
возраста пользователя или каких-то других признаков. Для этого применяется авторизация на основе claims. Собственно авторизация на основе ролей фактически представляет
частный случай авторизации на основе claims, так как роль это тот же объект Claim, имеющий тип ClaimsIdentity.DefaultRoleClaimType
.
Для авторизации на основе claims используются политики (policy). Политика представляет набор ограничений, которым должен соответствовать пользователь для доступа к ресурсу.
Все применяемые политики добавляются в приложение с помощью метода builder.Services.AddAuthorization()
.
Этот метод устанавливает политики с помощью объекта AuthorizationOptions. Например:
builder.Services.AddAuthorization(opts => { opts.AddPolicy("OnlyForMicrosoft", policy => { policy.RequireClaim("company", "Microsoft"); }); });
В данном случае добавляется политика с именем "OnlyForMicrosoft". И она требует обязательной установки для текущего пользователя объекта Claim с типом "company" и значением "Microsoft". Если для пользователя не будет установлено подобного объекта Claim, то такой пользователь не будет соответствовать политике.
Для управления политиками в классе AuthorizationOptions определены следующие свойства и методы:
DefaultPolicy: возвращает политику по умолчанию, которая используется, когда атрибут Authorize применяется без параметров
AddPolicy(name, policyBuilder): добавляет политику
GetPolicy(name): возвращает политику по имени
Ключевым методом здесь является AddPolicy()
. Первый параметр метода представляет название политики, а второй - делегат, который с помощью
объекта AuthorizationPolicyBuilder позволяет создать политику по определенным условиям. Для создания политики могут применяться
следующие методы класса AuthorizationPolicyBuilder:
RequireAuthenticatedUser(): пользователь обязательно должен быть аутентифицирован для соответствия политике
RequireClaim(type): для пользователя должен быть установлен claim с типом type. Причем не важно, какое значение будет иметь этот claim, главное, его наличие
RequireClaim(type, values): для пользователя должен быть установлен claim с типом type. Но теперь claim должен в качестве значения иметь одно из значений из массива values.
RequireRole(roles): пользователь должен принадлежать к одной из ролей из массива roles
RequireUserName(name): для соответствия политике пользователь должен иметь ник (логин) name
RequireAssertion(handler): запрос должен соответствовать условию, которое устанавливается с помощью делегата handler
AddRequirements(requirement): позволяет добавить кастомное ограничение requirement, если имеющихся недостаточно
Фактически данные методы задают ограничения, которым должен соответствовать пользователь, обращающийся к приложению. После установки ограничений политики в атрибуте Authorize можем их применять для разграничения доступа:
[Authorize(Policy = "OnlyForMicrosoft")]
Для установки политики у атрибута AuthorizeAttribute применяется свойство Policy. Оно указывает на название политики, которой должны соответствовать пользователи.
Допустим, у нас есть следующий класс, который представляет пользователя:
record class Person(string Email, string Password, string City, string Company);
У класса Person кроме свойств для хранения email и пароля также определено свойство City для хранения города и свойство Company для хранения компании пользователя.
Определим в приложении авторизацию на основе свойств City и Company. Для этого изменим код файла 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", "London", "Microsoft"), new Person("bob@gmail.com", "55555", "Лондон", "Google"), new Person("sam@gmail.com", "11111", "Berlin", "Microsoft") }; var builder = WebApplication.CreateBuilder(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/login"; options.AccessDeniedPath = "/login"; }); builder.Services.AddAuthorization(opts => { opts.AddPolicy("OnlyForLondon", policy => { policy.RequireClaim(ClaimTypes.Locality, "Лондон", "London"); }); opts.AddPolicy("OnlyForMicrosoft", policy => { policy.RequireClaim("company", "Microsoft"); }); }); 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.Locality, person.City), new Claim("company", person.Company) }; var claimsIdentity = new ClaimsIdentity(claims, "Cookies"); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); await context.SignInAsync(claimsPrincipal); return Results.Redirect(returnUrl ?? "/"); }); // доступ только для City = London app.Map("/london", [Authorize(Policy = "OnlyForLondon")]() => "You are living in London"); // доступ только для Company = Microsoft app.Map("/microsoft", [Authorize(Policy = "OnlyForMicrosoft")]() => "You are working in Microsoft"); app.Map("/", [Authorize](HttpContext context) => { var login = context.User.FindFirst(ClaimTypes.Name); var city = context.User.FindFirst(ClaimTypes.Locality); var company = context.User.FindFirst("company"); return $"Name: {login?.Value}\nCity: {city?.Value}\nCompany: {company?.Value}"; }); app.MapGet("/logout", async (HttpContext context) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return "Данные удалены"; }); app.Run(); record class Person(string Email, string Password, string City, string Company);
Здесь для тестирования механизма авторизации на основе Claims определена условная бд - список пользователей people:
var people = new List<Person> { new Person("tom@gmail.com", "12345", "London", "Microsoft"), new Person("bob@gmail.com", "55555", "Лондон", "Google"), new Person("sam@gmail.com", "11111", "Berlin", "Microsoft") };
Для настройки авторизации в зависимости от данных пользователя в делегате в методе AddAuthorization устанавливаются две политики доступа - "OnlyForLondon" и "OnlyForMicrosoft":
builder.Services.AddAuthorization(opts => { opts.AddPolicy("OnlyForLondon", policy => { policy.RequireClaim(ClaimTypes.Locality, "Лондон", "London"); }); opts.AddPolicy("OnlyForMicrosoft", policy => { policy.RequireClaim("company", "Microsoft"); }); });
Политика "OnlyForLondon" требует, чтобы claim с типом ClaimTypes.Locality
имел значение "London" или "Лондон".
Если значений много, то мы их можем перечислить через запятую. Вторая политика - "OnlyForMicrosoft" требует наличия Claim с типом
"company" и значением "Microsoft".
Для входа пользователей в приложение определена конечная точка 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.Locality, person.City), new Claim("company", person.Company) }; var claimsIdentity = new ClaimsIdentity(claims, "Cookies"); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
Благодаря этому инфраструктура ASP.NET Core сможет получить значения claims с типами ClaimTypes.Locality
и
"company"
(то есть соответственно город и компанию пользователей) и на их основе решить, предоставлять ли доступ пользователю к ресурсам приложения,
которые используют политику доступа на основе этих Claim.
Для тестирования политик доступа определены две конечных точки:
// доступ только для City = London app.Map("/london", [Authorize(Policy = "OnlyForLondon")]() => "You are living in London"); // доступ только для Company = Microsoft app.Map("/microsoft", [Authorize(Policy = "OnlyForMicrosoft")]() => "You are working in Microsoft");
Здесь доступ по пути "/london"
имеют только те пользователи, которые удовлетворяют политике "OnlyForLondon".
А ресурс "/microsoft"
доступен только для пользователей, соответствующих политике "OnlyForMicrosoft".
Запустим проект и залогинимся, используя данные одного из пользователей из списка people:
И если пользователь живет в Лондоне, то он имеет доступ по пути "/london". Аналогично если пользователь работает в Microsoft, он имеет доступ по пути "/microsoft":