Аутентификация и авторизация в ASP.NET Core. JWT и авторизация по ролям

Часто, для доступа к какому-либо ресурсу приложения недостаточно одного только факта аутентификации пользователя и необходимы дополнительные ограничения, например, авторизация по ролям. ASP.NET Core позволяет осуществлять авторизацию по ролям, используя рассмотренные нами ранее классы ClaimClaimsPrincipal и ClaimIdentity.

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

Определение ролей пользователя

Добавим в наше приложение три роли пользователей:

  1. USER — пользователи с этой ролью будут иметь доступ только к основной информации в приложении
  2. MANAGER — доступ к настройкам генерации токенов, добавление/удаление ролей конкретного пользователя
  3. ADMIN — доступ к любой части приложения
public static class UserRoles
{
    public const string USER = "USER";
    public const string MANAGER = "MANAGER";
    public const string ADMIN = "ADMIN";
}

Один и тот же пользователь может иметь несколько ролей, поэтому доработаем класс User следующим образом:

public class User
{
    public string Login { get; set; }
    public string Password { get; set; }
    public string Name { get; set; }   
    public string Email { get; set; }
    public int Age { get; set; }

    public List<string> Roles { get; set; } = new(); //роли пользователя

    public User(string login, string password, string name, string email, int age)
    {
        Login = login;
        Password = password;
        Name = name;
        Email = email;
        Age = age;
        Roles.Add(UserRoles.USER);//по умолчанию все пользователи имеют роль USER
    }

    public User(string login, string password, string name, string email, int age, string[] roles) : this(login, password, name, email, age)
    {
        Roles.AddRange(roles);
    }

    public IEnumerable<Claim> GetUserClaims()
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, Name, ClaimValueTypes.String),
            new Claim(ClaimTypes.Email, Email, ClaimValueTypes.Email),
            new Claim("Age", Age.ToString(), ClaimValueTypes.Integer)
        };
        //добавляем роли пользователя в список
        foreach (var role in Roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role));
        }
        return claims;
    }
}

здесь стоит обратить внимание на то, как роли добавляются в список Claim в методе GetUserClaims:

//добавляем роли пользователя в список
foreach (var role in Roles)
{
    claims.Add(new Claim(ClaimTypes.Role, role));
}

для каждого объекта типа Claim мы указываем тип Role, который в классе ClaimTypes представлен константой:

public const string Role = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role";

Работа с ролями пользователей в ASP.NET Core

Класс ClaimsPrincipal позволяет проверить относится ли пользователь к конкретной роли, используя метод

public virtual bool IsInRole(string role)

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

//условная база данных с учетными записями пользователей
List<User> UserDatabase = new()
{
    new User("user","12345","Иванов Иван Иванович", "user@mail.ru", 17, new[]{ UserRoles.MANAGER }),
    new User("admin","root","Петров Петр Петрович", "admin@mail.ru", 25, new[]{ UserRoles.MANAGER, UserRoles.ADMIN }),
};

перепишем обработчик конечной точки для главной страницы приложения следующим образом:

app.MapGet("/", (HttpContext context) => 
{
    string greetings = "Вы не аутентифицированы";
    string roleDesc = "Вы НЕ можете управлять приложением";

    if ((context.User != null) && (context.User.Identity.IsAuthenticated))
    {
        var claims = context.User.Claims;
        var name = claims.FirstOrDefault(f => f.Type == ClaimTypes.Name);
        if (name != null)
            greetings = $"Привет, {name.Value}";

        //проверяем пользователя на принадлежность роли ADMIN
        if (context.User.IsInRole("ADMIN"))
            roleDesc = "Вы можете управлять приложением";
    }

    return Results.Content( "<html><body>" +
        $"<p>{greetings}</p>" +
        $"<p>{roleDesc}</p>" +
        "</body></html>", "text/html; charset=utf-8");
});

Проверим работу приложения. Приложение после запуска

Аутентифицируем пользователя с ролью ADMIN:

Возвращаемся на главную страницу приложения

Теперь используем авторизацию по ролям.

Атрибут [Authorize] и авторизация по ролям

Для атрибута Authorize можно указывать различные параметры, в том числе и роли с которыми пользователи могут проходить авторизацию. Например, создадим следующую конечную точку приложения:

app.MapGet("/roles/{user}/add", [Authorize(Roles ="ADMIN,MANAGER")](string user, string role) => 
{
    var userData = UserDatabase.FirstOrDefault(f => f.Login == user);
    if (userData != null)
    {
        userData.Roles.Add(role);
        return $"Для пользователя {user} добавлена роль {role} \n Все роли пользователя: {string.Join(",", userData.Roles)}";
    }
    return $"Пользователь {user} не найден в БД";
});

Доступ к этой конечной точки имеют только пользователи, авторизованные с ролями ADMIN или MANAGER (список ролей перечисляется через запятую):

[Authorize(Roles ="ADMIN,MANAGER")]

для демонстрации работы, добавим ещё одного пользователя в список:

//условная база данных с учетными записями пользователей
List<User> UserDatabase = new()
{
    new User("user","12345","Иванов Иван Иванович", "user@mail.ru", 17, new[]{ UserRoles.MANAGER }),
    new User("admin","root","Петров Петр Петрович", "admin@mail.ru", 25, new[]{ UserRoles.MANAGER, UserRoles.ADMIN }),
    new User("john","12345","Сноу Джон", "snow@mail.ru", 35), //новый пользователь
};

Аутентифицируем нового пользователя и попробуем получить доступ к созданной конечной точке:

так как новый пользователь не относится к роли ADMIN или MANAGER, то при попытке получения доступа к конечной точке мы получаем ошибку доступа. Теперь зайдем в приложение с использованием учётной записи администратора и повторно попробуем получить доступ к конечной точке:

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

Исходный код проекта

Ниже представлен весь исходный код проекта с учётом изменений, затронутых в этой части

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace AspAuth
{
    public class AuthOptions
    {
        public string Issuer { get; set; } //издатель токена
        public string Audience { get; set; } //потребитель токена
        public string SecretKey { get; set; } //секретный ключ для подписи
        public bool ValidateAudience { get; set; } //проверять потребителя
        public bool ValidateIssuer { get; set; } //проверять издателя
        public bool ValidateLifetime { get; set; } //проверять время жизни токена
        public bool ValidateIssuerSigningKey { get; set; } //проверять подпись
        public int TokenLifetime { get; set; } //время жизни токена в минутах
    }

    public static class UserRoles
    {
        public const string USER = "USER";
        public const string MANAGER = "MANAGER";
        public const string ADMIN = "ADMIN";
    }

    public class User
    {
        public string Login { get; set; }
        public string Password { get; set; }
        public string Name { get; set; }   
        public string Email { get; set; }
        public int Age { get; set; }

        public List<string> Roles { get; set; } = new();

        public User(string login, string password, string name, string email, int age)
        {
            Login = login;
            Password = password;
            Name = name;
            Email = email;
            Age = age;
            Roles.Add(UserRoles.USER);//по умолчанию все пользователи имеют роль USER
        }

        public User(string login, string password, string name, string email, int age, string[] roles) : this(login, password, name, email, age)
        {
            Roles.AddRange(roles);
        }

        public IEnumerable<Claim> GetUserClaims()
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, Name, ClaimValueTypes.String),
                new Claim(ClaimTypes.Email, Email, ClaimValueTypes.Email),
                new Claim("Age", Age.ToString(), ClaimValueTypes.Integer)
            };
            //добавляем роли пользователя в список
            foreach (var role in Roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role));
            }
            return claims;
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {

            //условная база данных с учетными записями пользователей
            List<User> UserDatabase = new()
            {
                new User("user","12345","Иванов Иван Иванович", "user@mail.ru", 17, new[]{ UserRoles.MANAGER }),
                new User("admin","root","Петров Петр Петрович", "admin@mail.ru", 25, new[]{ UserRoles.MANAGER, UserRoles.ADMIN }),
                new User("john","12345","Сноу Джон", "snow@mail.ru", 35), //новый пользователь
            };

            var builder = WebApplication.CreateBuilder(args);
            var authOptions = builder.Configuration.GetSection("Jwt").Get<AuthOptions>();
            SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authOptions.SecretKey));
            builder.Services.AddAuthentication(auth =>
            {
                auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
                .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
                {
                    options.RequireHttpsMetadata = true;
                    options.SaveToken = true;
                    options.IncludeErrorDetails = true;
                    
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = authOptions.ValidateIssuer,
                        ValidateAudience = authOptions.ValidateAudience,
                        ValidateLifetime = authOptions.ValidateLifetime,
                        ValidateIssuerSigningKey = authOptions.ValidateIssuerSigningKey,
                        ValidIssuer = authOptions.Issuer,
                        ValidAudience = authOptions.Audience,
                        IssuerSigningKey = key
                    };
                });
            builder.Services.AddAuthorization();
            var app = builder.Build();

            app.Use(async (context, next) =>
            {
                if (context.Request.Cookies.TryGetValue("token", out string? token))
                    context.Request.Headers.Authorization = $"Bearer {token}";
               
                await next();
            });


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

            app.MapGet("/", (HttpContext context) => 
            {
                string greetings = "Вы не аутентифицированы";
                string roleDesc = "Вы НЕ можете управлять приложением";

                if ((context.User != null) && (context.User.Identity.IsAuthenticated))
                {
                    var claims = context.User.Claims;
                    var name = claims.FirstOrDefault(f => f.Type == ClaimTypes.Name);
                    if (name != null)
                        greetings = $"Привет, {name.Value}";

                    //проверяем пользователя на принадлежность роли ADMIN
                    if (context.User.IsInRole("ADMIN"))
                        roleDesc = "Вы можете управлять приложением";
                }

                return Results.Content( "<html><body>" +
                    $"<p>{greetings}</p>" +
                    $"<p>{roleDesc}</p>" +
                    "</body></html>", "text/html; charset=utf-8");
            });


            app.MapGet("/roles/{user}/add", [Authorize(Roles ="ADMIN,MANAGER")](string user, string role) => 
            {
                var userData = UserDatabase.FirstOrDefault(f => f.Login == user);
                if (userData != null)
                {
                    userData.Roles.Add(role);
                    return $"Для пользователя {user} добавлена роль {role} \n Все роли пользователя: {string.Join(",", userData.Roles)}";
                }
                return $"Пользователь {user} не найден в БД";
            });


            app.MapGet("/auth", [Authorize(Roles ="ADMIN")] (HttpContext context) =>
            {
                return Results.Ok(authOptions);
            });
            app.MapGet("/login", (string? login, string? password, HttpContext context) =>
            {
                if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password))
                    return Results.BadRequest(new { Error = "Не задан логин или пароль" });
                var user = UserDatabase.FirstOrDefault(u => (u.Login == login) && (u.Password == password));
                if (user == null)
                    return Results.BadRequest(new { Error = "Не верно введен логин или пароль" });

                JwtSecurityToken token = new(
                    issuer: authOptions?.Issuer,
                    audience: authOptions?.Audience,
                    claims: user.GetUserClaims(), //добавляем список Claim в токен
                    expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(authOptions.TokenLifetime)),
                    signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
                );
                
                JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
                string tokenString = handler.WriteToken(token);

                context.Response.Cookies.Append("token", tokenString);//отпраляем клиенту куку с токеном

                return Results.Ok(new { token = tokenString, user = login });
            });
            app.Run();
        }
    }
}

Итого

Для использования авторизации пользователей по ролям при работе с JWT мы должны определить роли пользователей, которые в самом простом случае являются в приложении обычными строками. Все роли записываются в список объектов Claim пользователя и сохраняются в токене JWT. Авторизация по ролям в ASP.NET Core реализуется путем добавления к методу атрибута Authorize с параметром Roles в котором через запятую перечисляются все роли пользователей для которых разрешен доступ к ресурсу.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии