Содержание
Часто, для доступа к какому-либо ресурсу приложения недостаточно одного только факта аутентификации пользователя и необходимы дополнительные ограничения, например, авторизация по ролям. ASP.NET Core позволяет осуществлять авторизацию по ролям, используя рассмотренные нами ранее классы Claim, ClaimsPrincipal и ClaimIdentity.
Допишем наше приложение для аутентификации и авторизации, используя различные роли пользователей.
Определение ролей пользователя
Добавим в наше приложение три роли пользователей:
- USER — пользователи с этой ролью будут иметь доступ только к основной информации в приложении
- MANAGER — доступ к настройкам генерации токенов, добавление/удаление ролей конкретного пользователя
- 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 можно указывать различные параметры, в том числе и роли с которыми пользователи могут проходить авторизацию. Например, создадим следующую конечную точку приложения:
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 в котором через запятую перечисляются все роли пользователей для которых разрешен доступ к ресурсу.





