Авторизация по ролям

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

Авторизация по ролям позволяет разграничить доступ к ресурсам приложения в зависимости от роли, к которой принадлежит пользователь.

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

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":

Авторизация по ролям в ASP.NET Core и C#

Однако при попытке пользователя с ролью "user" обратиться по адресу "/admin", он будет переадресован на адрес "/accessdenied":

Ограничение доступа по ролям в ASP.NET Core и C#

Стоит отметить, что при аутентификации куки при перенаправлении по пути из свойства options.AccessDeniedPath автоматически передается параметр ReturnUrl, из которого можно получить путь, к которому пытался обращаться пользователь.

Теперь выйдем из приложения и снова залогинимся, только теперь под пользователем с ролью "admin":

Роли пользователей и авторизация в приложении ASP.NET Core и C#

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

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