Содержание
В предыдущей части мы разобрались с тем, как организовать авторизацию пользователей по ролям, используя 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, что не соответствует названию. Поэтому в следующей части мы рассмотрим вопрос создания собственных требований к авторизации в которых и затронем вопрос об использовании требований типа «не менее», «содержит что-либо» и т.д.



