Содержание
В предыдущей части мы рассмотрели пример авторизации пользователя на основе политик и остановились на том, что, несмотря на то, что встроенные возможности ASP.NET Core позволяют покрыть максимум потребностей разработчиков, всё же иногда необходимо добавить в работу политики авторизации пользователя своё ограничение, например, таковым является ограничение доступа пользователя по его возрасту. И сегодня мы рассмотрим вопрос создания собственных ограничений авторизации в ASP.NET Core.
На данный момент, у класса пользователя в нашем приложении определено свойство Age
, которое хранит возраст пользователя:
public class User { //...// public int Age { get; set; } //...//
Создадим новое ограничение, которое будет учитывать значение этого свойства при авторизации пользователя. Класс ограничения должен реализовывать интерфейс IAuthorizationRequirement
, который выглядит следующим образом:
public interface IAuthorizationRequirement
то есть не содержит ничего — ни свойств, ни методов. Таким образом, создадим следующее ограничение:
public class MinimumAgeRequirement : IAuthorizationRequirement { protected int Age { get; set; } public MinimumAgeRequirement(int age) { Age = age; } }
Сам по себе, этот класс не выполняет никаких действий, а только хранит некоторое ограничение, как в нашем случае — минимально допустимый возраст пользователя. Чтобы использовать это ограничение при обработке запроса пользователя нам необходимо реализовать обработчик авторизации, который будет использовать созданное ограничение.
Обработчик авторизации должен наследовать абстрактный класс AuthorizationHandler<TRequirement>
и реализовывать его метод
protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement);
Этот метод вызывается системой авторизации ASP.NET Core при доступе к ресурсу для которого применяется ограничение, используемое обработчиком. Рассмотрим реализацию обработчика авторизации на примере нашего ограничения:
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement) { // получаем claim с типом Age - возраст var ageClaim = context.User.FindFirst(c => c.Type == "Age"); if (ageClaim is not null) { // если claim хранит число if (int.TryParse(ageClaim.Value, out var age)) { if (requirement.Age <= age) context.Succeed(requirement); // сигнализируем, что claim соответствует ограничению } } return Task.CompletedTask; } }
В параметрах метод HandleRequirementAsync()
получает объект применяемого ограничения (MinimumAgeRequirement
) и контекст авторизации AuthorizationHandlerContext
, в котором содержится информация о запросе и, в частности, свойство User
, используя которое, мы можем получить доступ ко всем объектам Claim
пользователя.
Методы класса AuthorizationHandlerContext
позволяют управлять авторизацией. Так, метод Succeed(requirement)
вызывается, если запрос соответствует ограничению requirement и, наоборот, метод Fail()
, если запрос не соответствует ограничению.
Таким образом, выше мы сделали следующее:
- получили из объекта
User
объектClaim
с типомAge
- проверили, что
Claim
содержит число (возраст) - проверили, что возраст пользователя больше или равен значению в ограничении. При выполнении условия выполнили метод
Succeed()
Применение обработчика авторизации в приложении ASP.NET Core
Чтобы применить свой обработчик авторизации, мы должны его зарегистрировать как сервис и добавить ограничение в список Requirements
для политики авторизации. Добавим в политику авторизации, которую мы создали в прошлой части, новое ограничение. Ниже представлен только новый код проекта, а весь код будет опубликован в конце этой части:
public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Services.AddTransient<IAuthorizationHandler, MinimumAgeHandler>(); //добавляем свой обработчик авторизации //настраиванием политику авторизации пользователей builder.Services.AddAuthorization((options) => { options.AddPolicy("OnlyForMicrosoftAdmin", policy => { policy.RequireClaim("Organization", "Microsoft").//должен работать в Microsoft RequireClaim("Department", "Development").//долден работать в отделе Development RequireRole(new[] { "ADMIN" })//должен быть админом .Requirements.Add(new MinimumAgeRequirement(18)); //должен быть не моложе 18 лет }); }); //применяем политику авторизации для конечной точки app.MapGet("/roles/{user}/add", [Authorize(Policy = "OnlyForMicrosoftAdmin")] (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.Run(); } } }
Здесь мы добавили ограничение MinimumAgeRequirement
в политику авторизации с именем OnlyForMicrosoftAdmin
. Протестируем наше приложение. Для этого добавим в список пользователей следующего пользователя:
//условная база данных с учетными записями пользователей List<User> UserDatabase = new() { new User("user","12345","Иванов Иван Иванович", "user@mail.ru", 17, new[]{ UserRoles.MANAGER }, "MyCompany", "Administration"), new User("admin","root","Петров Петр Петрович", "admin@mail.ru", 17, new[]{ UserRoles.MANAGER, UserRoles.ADMIN }, "Microsoft", "Development"), new User("john","12345","Сноу Джон", "snow@mail.ru", 35, "Cisco", "Security"), //пользователь, соответствующий ограничению по возрасту new User("microsoft","root","Билли Гейтс", "admin@mail.ru", 45, new[]{ UserRoles.MANAGER, UserRoles.ADMIN }, "Microsoft", "Development"), };
Таким образом, у нас есть два пользователя с ролью ADMIN
, но, при этом, пользователь admin
не подходит по возрасту, а пользователь microsoft
— соответствует всем требованиям авторизации. Зарегистрируемся с логином admin:
теперь попытаемся получить доступ к конечной точке с авторизацией:
Теперь проверим пользователя с логином microsoft
:
Как можно увидеть, наше ограничение по возрасту работает и пользователь с возрастом старше 18 лет получил доступ к конечной точке, что соответствует политике авторизации.
Исходный код проекта
Ниже представлен весь исходный код проекта, реализующего процессы аутентификации и авторизации пользователей с использованием токена JWT
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 MinimumAgeRequirement : IAuthorizationRequirement { protected internal int Age { get; set; } public MinimumAgeRequirement(int age) { Age = age; } } public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement) { // получаем claim с типом Age - возраст var ageClaim = context.User.FindFirst(c => c.Type == "Age"); if (ageClaim is not null) { // если claim хранит число if (int.TryParse(ageClaim.Value, out var age)) { if (requirement.Age <= age) context.Succeed(requirement); // сигнализируем, что claim соответствует ограничению else context.Fail(); } } return Task.CompletedTask; } } 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 string Organization { get; set; } public string Department { 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, string organization, string department) { Login = login; Password = password; Name = name; Email = email; Age = age; Roles.Add(UserRoles.USER);//по умолчанию все пользователи имеют роль USER Organization = organization; Department = department; } public User(string login, string password, string name, string email, int age, string[] roles, string organization, string department) : this(login, password, name, email, age, organization, department) { 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), new Claim("Organization", Organization), new Claim("Department", Department), }; //добавляем роли пользователя в список 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 }, "MyCompany", "Administration"), new User("admin","root","Петров Петр Петрович", "admin@mail.ru", 17, new[]{ UserRoles.MANAGER, UserRoles.ADMIN }, "Microsoft", "Development"), new User("john","12345","Сноу Джон", "snow@mail.ru", 35, "Cisco", "Security"), //пользователь, соответствующий ограничению по возрасту new User("microsoft","root","Билли Гейтс", "admin@mail.ru", 45, new[]{ UserRoles.MANAGER, UserRoles.ADMIN }, "Microsoft", "Development"), }; var builder = WebApplication.CreateBuilder(args); var authOptions = builder.Configuration.GetSection("Jwt").Get<AuthOptions>(); SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authOptions.SecretKey)); builder.Services.AddTransient<IAuthorizationHandler, MinimumAgeHandler>(); //добавляем свой обработчик авторизации 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((options) => { options.AddPolicy("OnlyForMicrosoftAdmin", policy => { policy.RequireClaim("Organization", "Microsoft").//должен работать в Microsoft RequireClaim("Department", "Development").//долден работать в отделе Development RequireRole(new[] { "ADMIN" })//должен быть админом .Requirements.Add(new MinimumAgeRequirement(18)); //должен быть не моложе 18 лет }); }); 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(Policy = "OnlyForMicrosoftAdmin")] (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(); } } }
Итого
Для создания собственных ограничений авторизации мы должны выполнить следующие шаги: 1) создать класс ограничения, который будет реализовывать интерфейс IAuthorizationRequirement
и содержать какие-либо значения на которых будет основываться ограничение; 2) создать обработчик авторизации, унаследовав его от абстрактного класса AuthorizationHandler<T>
и реализовать метод HandleRequirementAsync
в котором необходимо проверить соответствие Claim
пользователя ограничению и, в зависимости от результата проверки выполнить метод Succeed()
(пользователь соответствует ограничению) или Fail()
класса AuthorizationHandlerContext
. 3) зарегистрировать обработчик авторизации в качестве сервиса и добавить свое ограничение в список Requirements
политики авторизации ASP.NET Core.