Содержание
В предыдущей части мы разобрались с тем, как организовать авторизацию пользователей по ролям, используя JWT-токены. Однако, авторизация по ролям — это не единственная возможность ASP.NET Core. Иногда бывает необходимо, чтобы пользователь соответствовал не одному критерию (принадлежности к конкретной роли), а сразу к группе критериев, например, должен быть определенного возраста, работать в определенной компании и иметь определенную должность. И в этом случае, разделение по ролям уже мало, чем нам поможет так как для авторизации приходится проводить анализ ряда критериев. В этом случае мы можем использовать различные политики авторизации.
Политики авторизации
Политика авторизации — это некий набор критериев, которыми должен обладать клиент для доступа к ресурсу. Как мы уже знаем, для каждого пользователя можно задавать различные свойства (утверждения, требования) с использованием коллекции объектов типа Claim
. Именно с использованием Claim
и реализуются в ASP.NET Core различные политики авторизации. И так как коллекция объектов Claim для пользователя может быть самой разнообразной, это позволяет организовать гибкую систему авторизации в вашем приложении.
Рассмотрим применение политик авторизации на примере нашего приложения, использующего для аутентификации JWT-токены. Последнюю версию исходного кода приложения можно посмотреть здесь.
Настройка политик авторизации в ASP.NET Core
Все настройки политик авторизации в ASP.NET Core осуществляются при добавлении в проект сервиса авторизации методом AddAuthorization()
, например, следующим образом:
builder.Services.AddAuthorization((options) => { options.AddPolicy("MinimumAgePolicy", policy => { policy.RequireClaim("Age","18"); }); });
здесь options
представляет собой объект типа AuthorizationOptions
с помощью которого мы управляем политиками авторизации с помощью следующих свойств и методов
Свойство | Default |
Возвращает или задает политику авторизации по умолчанию, которая будет использоваться, если атрибут Authorize задается без параметров |
Метод | Add |
Добавьте политику с указанным именем, созданную на основе делегата . |
Метод | Get |
Возвращает политику для указанного имени или значение NULL , если политика с этим именем не существует. |
Для нас, на данный момент, важным является метод Add
public void AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy)
первый параметр метода — имя новой политики авторизации, а второй — делегат, в котором мы, используя AuthorizationPolicyBuilder
настраиваем политику. Класс AuthorizationPolicyBuilder
содержит довольно много свойств и методов, позволяющих настраивать и объединять различные политики авторизации. Приведем основные из них:
Requirements |
Возвращает или задает список требований, которые должны выполняться чтобы пользователь был авторизован |
Add |
Добавляет новое требование в список Requirements. |
Require |
Пользователь должен быть аутентифицирован, чтобы чтобы соответствовать политике |
Require |
У пользователя должен быть определенный Claim , чтобы соответствовать политике. При этом, значение Claim не проверяется — только наличие |
Require |
у пользователя должен быть определен Claim с типом type и одним из значений, перечисленных в values |
Require |
у пользователя должны быть определена хотя бы одна роль из списка values |
Require |
пользователь должен иметь определенное имя, чтобы соответствовать политике |
Таким образом, выше мы определили политику авторизации с именем MinimumAgePolicy
в которой установили ограничение — пользователь должен иметь Claim
с именем «Age» и значением «18».
Применение политик авторизации
Для применения политик авторизации в ASP.NET Core мы можем либо воспользоваться атрибутом Authorize, указав в нем имя политики:
[Authorize(Policy = "MinimumAgePolicy")]
либо использовать метод расширения для AuthorizationEndpointConventionBuilder
при создании конечной точки:
app.MapGet("/roles/{user}/add", (string user, string role) => { //здесь код для конечной точки }).RequireAuthorization(policyNames: new[] { "MinimumAgePolicy"});
В метод RequireAuthorization
необходимо передать массив содержащий имена политик авторизации, которые будут применены к конечной точке.
Использование политик авторизации в ASP.NET Core при аутентификации с использованием JWT
Вернемся к нашему приложению для аутентификации и авторизации пользователей и настроим свою политику авторизации пользователя. Исходный код проекта можно посмотреть здесь. На данный момент класс пользователя системы имеет следующее описание:
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; } }
Добавим к этому классу ещё два свойства — организацию и отдел в котором работает пользователь и будем передавать эти свойства в коллекции Claim
public class User { //...// public string Organization { get; set; } public string Department { get; set; } //...// public User(string login, string password, string name, string email, int age, string organization, string department) { //...// 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("Organization", Organization), new Claim("Department", Department), }; //...// } }
Соответственно, список пользователей, представляющий собой условную БД в методе Main()
проекта будет теперь формироваться следующим образом:
//условная база данных с учетными записями пользователей List<User> UserDatabase = new() { new User("user","12345","Иванов Иван Иванович", "user@mail.ru", 17, new[]{ UserRoles.MANAGER }, "MyCompany", "Administration"), new User("admin","root","Петров Петр Петрович", "admin@mail.ru", 18, new[]{ UserRoles.MANAGER, UserRoles.ADMIN }, "Microsoft", "Development"), new User("john","12345","Сноу Джон", "snow@mail.ru", 35, "Cisco", "Security"), //новый пользователь };
Теперь создадим новую политику авторизации, в которой будут установлены следующие требования к пользователю — он должен работать в организации Microsoft
в отделе Development
и иметь роль администратора (ADMIN
):
builder.Services.AddAuthorization((options) => { options.AddPolicy("OnlyForMicrosoftAdmin", policy => { policy.RequireClaim("Organization","Microsoft").//должен работать в Microsoft RequireClaim("Department","Development").//должен работать в отделе Development RequireRole(new[] { "ADMIN"}); //должен быть админом }); });
Теперь применим эту политику авторизации к конечной точке в которой мы управляли ролями пользователей:
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} не найден в БД"; });
Этой политике у нас соответствует только один пользователь с логином admin
. Проверим работу приложения. Авторизуемся с логином john
попробуем получить доступ к конечной точке добавления ролей пользователей:
В доступе отказано, так как пользователь не соответствует политике. Теперь авторизуемся с логином admin и попробуем повторить операцию:
Как можно увидеть, пользователь admin полностью соответствовал политике авторизации, поэтому его запрос на добавление новой роли прошел успешно.
Исходный код проекта
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 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", 18, new[]{ UserRoles.MANAGER, UserRoles.ADMIN }, "Microsoft", "Development"), new User("john","12345","Сноу Джон", "snow@mail.ru", 35, "Cisco", "Security"), //новый пользователь }; 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((options) => { options.AddPolicy("OnlyForMicrosoftAdmin", policy => { policy.RequireClaim("Organization","Microsoft").//должен работать в Microsoft RequireClaim("Department","Development").//долден работать в отделе Development RequireRole(new[] { "ADMIN"}); //должен быть админом }); }); 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(); } } }
Итого
Сегодня мы познакомились с ещё одним способом авторизации пользователей — на основе политик. Политики позволяют сделать систему авторизации более гибкой и предъявлять к пользователю самые различные требования для успешной авторизации. Несмотря на то, что ASP.NET Core предоставляет нам довольно широкие возможности по формированию политик, всё же их бывает недостаточно. Например, политика авторизации, рассмотренная в самом начале этой части с названием MinimumAgePolicy
на самом деле будет ограничивать всех пользователей возраст которых не равен 18, что не соответствует названию. Поэтому в следующей части мы рассмотрим вопрос создания собственных требований к авторизации в которых и затронем вопрос об использовании требований типа «не менее», «содержит что-либо» и т.д.