Авторизация по ролям позволяет разграничить доступ к ресурсам приложения в зависимости от роли, к которой принадлежит пользователь.
Допустим, у нас есть следующие классы, которые описывают пользователя и его роль:
class Person { public string Email { get; set; } public string Password { get; set; } public Role Role { get; set; } public Person(string email, string password, Role role) { Email = email; Password = password; Role = role; } } class Role { public string Name { get; set; } public Role(string name) => Name = name; }
Класс роли содержит свойство Name, которое хранит название роли. А класс Person хранит email-адрес, пароль и роль пользователя.
В файле Program.cs определим следующий код:
using Microsoft.AspNetCore.Authentication.Cookies; using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; var adminRole = new Role("admin"); var userRole = new Role("user"); var people = new List<Person> { new Person("tom@gmail.com", "12345", adminRole), new Person("bob@gmail.com", "55555", userRole), }; var builder = WebApplication.CreateBuilder(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/login"; options.AccessDeniedPath = "/accessdenied"; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); // добавление middleware авторизации app.MapGet("/accessdenied", async (HttpContext context) => { context.Response.StatusCode = 403; await context.Response.WriteAsync("Access Denied"); }); 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(ClaimsIdentity.DefaultNameClaimType, person.Email), new Claim(ClaimsIdentity.DefaultRoleClaimType, person.Role.Name) }; var claimsIdentity = new ClaimsIdentity(claims, "Cookies"); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); await context.SignInAsync(claimsPrincipal); return Results.Redirect(returnUrl ?? "/"); }); // доступ только для роли admin app.Map("/admin", [Authorize(Roles = "admin")]() => "Admin Panel"); // доступ только для ролей admin и user app.Map("/", [Authorize(Roles = "admin, user")](HttpContext context) => { var login = context.User.FindFirst(ClaimsIdentity.DefaultNameClaimType); var role = context.User.FindFirst(ClaimsIdentity.DefaultRoleClaimType); return $"Name: {login?.Value}\nRole: {role?.Value}"; }); app.MapGet("/logout", async (HttpContext context) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return "Данные удалены"; }); app.Run();
Для упрощения ситуации здесь данные ролей и пользователей определены напрямую в коде в виде списка people:
var adminRole = new Role("admin"); var userRole = new Role("user"); var people = new List<Person> { new Person("tom@gmail.com", "12345", adminRole), new Person("bob@gmail.com", "55555", userRole), };
Здесь определены две роли - "admin" и "user" и два пользователя.
Для аутентификации здесь используются куки:
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/login"; options.AccessDeniedPath = "/accessdenied"; });
Здесь свойство options.AccessDeniedPath
указывает на путь, на который будет перенаправляться аутентифицированный пользователь
при обращении к ресурсу, для доступа к котоому у него нет прав. То есть важно понимать разницу между назначением свойств options.LoginPath
и
options.AccessDeniedPath
:
options.LoginPath
: определяет путь перенаправления для не аутентифицированного пользователя
options.AccessDeniedPath
: определяет путь перенаправления для аутентифицированного пользователя, который не имеет прав для доступа к ресурсу
В реальности для обоих параметров можно использовать один и тот же путь. Но в данном случае я их разграничил.
Таким образом, при доступе к ресурсу, для которого у пользователя нет прав, пользователь перенаправляется по адресу "/accessdenied". Запрос по этому пути обрабатывается следующей конечной точкой:
app.MapGet("/accessdenied", async (HttpContext context) => { context.Response.StatusCode = 403; await context.Response.WriteAsync("Access Denied"); });
Здесь просто отправляется сообщение о запрете доступа со статусным кодом 403.
Если пользователь не аутентифицирован, то его перенаправляет по пути "/login". GET-запрос по этому пути
обрабатывается конечной точкой app.MapGet("/login")
, которая отправляет пользователю форму для ввода логина и пароля.
Отправленные пользователем в POST-запросе данные логина и пароля будут обрабатываться конечной точкой app.MapPost("/login")
.
Ключевым моментом ее обработчика является установка списка объектов claim, в которых сохраняется логин пользователя и его роль:
var claims = new List<Claim> { new Claim(ClaimsIdentity.DefaultNameClaimType, person.Email), new Claim(ClaimsIdentity.DefaultRoleClaimType, person.Role.Name) };
Для указания роли здесь применяется тип claim ClaimsIdentity.DefaultRoleClaimType, а в качестве значения для этого типа используется имя роли. По сути больше для установки роли для пользователя ничего не нужно.
Чтобы на уровне отдельных ресурсов приложения разграничить доступ в зависимости от роли, свойству Roles
атрибута Authorize
передается набор допустимых ролей:
[Authorize(Roles = "admin")
Можно передавать несколько ролей через запятую:
[Authorize(Roles = "admin, user")
Для определения роли текущего пользователя инфрастуктура ASP.NET Core будет использовать значения claim с типом ClaimsIdentity.DefaultRoleClaimType.
Например, обращаться по пути "/admin" могут только пользователи, которые принадлежат роли "admin":
app.Map("/admin", [Authorize(Roles = "admin")]() => "Admin Panel");
В то время как по пути "/" могут обращаться представлители ролей "user" и "admin":
app.Map("/", [Authorize(Roles = "admin, user")](HttpContext context) => { var login = context.User.FindFirst(ClaimsIdentity.DefaultNameClaimType); var role = context.User.FindFirst(ClaimsIdentity.DefaultRoleClaimType); return $"Name: {login?.Value}\nRole: {role?.Value}"; });
Например, залогинимся в приложение пользователем, который имеет роль "user":
Однако при попытке пользователя с ролью "user" обратиться по адресу "/admin", он будет переадресован на адрес "/accessdenied":
Стоит отметить, что при аутентификации куки при перенаправлении по пути из свойства options.AccessDeniedPath
автоматически передается параметр
ReturnUrl
, из которого можно получить путь, к которому пытался обращаться пользователь.
Теперь выйдем из приложения и снова залогинимся, только теперь под пользователем с ролью "admin":
Таким образом, мы можем разграничивать доступ в приложении в зависимости от роли пользователя.