В прошлой теме был рассмотрен объект HttpContext.User
, который представляет класс ClaimsPrincipal и который хранит данные пользователя в
свойстве Identity
в виде объекта ClaimsIdentity
. Но что представляют сами данные пользователя? Для
хранения различных данных пользователя во фреймворке ASP.NET Core определяются объекты claim.
Объекты claim представляют некоторую информацию о пользователе, которую мы можем использовать для авторизации в приложении. Например, у пользователя может быть определенный возраст, город, страна проживания, любимая музыкальная группа и прочие признаки. И все эти признаки могут представлять отдельные объекты claim. И в зависимости от значения этих claim мы можем предоставлять пользователю доступ к тому или иному ресурсу. Таким образом, claims представляют более общий механизм авторизации нежели стандартные логины или роли, которые привязаны лишь к одному определенному признаку пользователя.
Каждый объект claim представляет класс Claim из пространства имен System.Security.Claims
, который определяет следующие свойства:
Issuer: "издатель" или название системы, которая выдала данный claim
Subject: возвращает информацию о пользователе в виде объекта ClaimsIdentity
Type: возвращает тип объекта claim
Value: возвращает значение объекта claim
Для создания объекта Claim определено множество конструкторов, но чаще всего применяется следующая версия конструктора:
public Claim(string type, string value)
В качестве первого параметра в конструктор передается тип claima - это некоторая строка, которая, как правило, описывает назначение claima. В качестве второго параметра передается значение этого claima. Например, простейшее создание claima:
var usernameClaim = new Claim(ClaimTypes.Name, "Tom");
В качестве типов можно использовать встроенные константы, типа ClaimTypes.Name
,
которая имеет значение "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" и которая обычно применяется для установки имени пользователя (то, что потом мы
сможем получить через свойство HttpContext.User.Identity.Name). И в данном случае этот claim будет иметь значение "Tom".
Все объекты claim, которые описывают пользователя, затем можно передать в виде коллекции в конструктор ClaimsIdentity:
var usernameClaim = new Claim(ClaimTypes.Name, "Tom"); var claims = new List<Claim> { usernameClaim }; var claimsIdentity = new ClaimsIdentity(claims, "Cookies");
Рассмотрим на небольшом примере:
using Microsoft.AspNetCore.Authentication.Cookies; using System.Security.Claims; using Microsoft.AspNetCore.Authentication; var builder = WebApplication.CreateBuilder(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(); var app = builder.Build(); app.UseAuthentication(); app.MapGet("/login/{username}", async (string username, HttpContext context) => { var claims = new List<Claim> { new (ClaimTypes.Name, username) }; var claimsIdentity = new ClaimsIdentity(claims, "Cookies"); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); await context.SignInAsync(claimsPrincipal); return $"Установлено имя {username}"; }); app.Map("/", (HttpContext context) => { var user = context.User.Identity; if (user is not null && user.IsAuthenticated) return $"UserName: {user.Name}"; else return "Пользователь не аутентифицирован."; }); app.MapGet("/logout", async (HttpContext context) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return "Данные удалены"; }); app.Run();
В данном примере в конечной точке app.MapGet("/login/{username}")
через параметр "username" получаем некоторое условное имя
пользователя, создаем из него Claim, передаем его в ClaimsIdentity. В итоге после этот claim будет сохранен в аутентификационных куках. И
когда в следующем запросе приложение получит эти аутентификационные куки, оно сможет извлечь эти данные и использовать их при создании объекта ClaimsPrincipal.
Следует отметить, что по умолчанию значение claim с типом ClaimTypes.Name
(либо ClaimsIdentity.DefaultNameClaimType
)
передается свойству HttpContext.User.Identity.Name.
Для работы с объектами Claim в классе ClaimsPrincipal есть следующие свойства и методы:
Claims: свойство, которое возвращает набор ассоциированных с пользователем объектов claim
FindAll(type) / FindAll(predicate): возвращает все объекты claim, которые соответствуют определенному типу или условию
FindFirst(type) / FindFirst(predicate): возвращает первый объект claim, который соответствуют определенному типу или условию
HasClaim(type, value) / HasClaim(predicate): возвращает значение true
, если пользователь имеет claim определенного типа с определенным значением
IsInRole(name): возвращает значение true
, если пользователь принадлежит роли с названием name
С помощью объекта ClaimsIdentity, который возвращается свойством User.Identity
, мы можем управлять объектами claim у текущего пользователя.
В частности, класс ClaimsIdentity определяет следующие свойства и методы:
Claims: свойство, которое возвращает набор ассоциированных с пользователем объектов claim
AddClaim(claim): добавляет для пользователя объект claim
AddClaims(claims): добавляет набор объектов claim
FindAll(type) / FindAll(predicate): возвращает все объекты claim, которые соответствуют определенному типу или условию
FindFirst(type) / FindFirst(predicate): возвращает первый объект claim, который соответствуют определенному типу или условию
HasClaim(predicate): возвращает значение true
, если пользователь имеет claim, соответствующий определенному условию
RemoveClaim(claim): удаляет объект claim
TryRemoveClaim(claim): удаляет объект claim и возвращает true при успешном удалении
Например, определим у пользователя несколько объектов claim:
using Microsoft.AspNetCore.Authentication.Cookies; using System.Security.Claims; using Microsoft.AspNetCore.Authentication; var builder = WebApplication.CreateBuilder(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(); var app = builder.Build(); app.UseAuthentication(); app.MapGet("/login", async (HttpContext context) => { var username = "Tom"; var company = "Microsoft"; var phone = "+12345678901"; var claims = new List<Claim> { new Claim (ClaimTypes.Name, username), new Claim ("company", company), new Claim(ClaimTypes.MobilePhone,phone) }; var claimsIdentity = new ClaimsIdentity(claims, "Cookies"); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); await context.SignInAsync(claimsPrincipal); return Results.Redirect("/"); }); app.Map("/", (HttpContext context) => { // аналогично var username = context.User.Identity.Name var username = context.User.FindFirst(ClaimTypes.Name); var phone = context.User.FindFirst(ClaimTypes.MobilePhone); var company = context.User.FindFirst("company"); return $"Name: {username?.Value}\nPhone: {phone?.Value}\nCompany: {company?.Value}"; }); app.MapGet("/logout", async (HttpContext context) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return "Данные удалены"; }); app.Run();
В конечной точке app.MapGet("/login")
в куки сохраняются объект ClaimsPrincipal с тремя объектами claims. Они представляют имя, компанию и телефон пользователя.
Причем для добавления некоторых claim мы можем воспользоваться встроенными типами, как для имени или телефона пользователя:
new Claim(ClaimTypes.MobilePhone,phone)
Для других данных мы можем определить свои типы, просто передав какую-нибудь строку, как в случае с компанией:
new Claim ("company", company),
Затем в приложении при обработке запроса мы можем получить эти данные. Как в примере выше в конечной точке app.Map("/")
:
var phone = context.User.FindFirst(ClaimTypes.MobilePhone); var company = context.User.FindFirst("company");
В метод FindFirst
передается тип claim. Стоит учитывать, что этот метод возвращает объект Claim?
, то есть результатом метода
может быть значение null (например, если пользователь не аутентифицирован или объект claim не установлен).
Соответственно при обращении к значению claim необходимо проверять его на null.
Таким образом, после обращения по пути "/login" в куках будут сохранены данные пользователя:
Если мы динамически решим добавить новый claim или удалить существующий, то после изменения claim необходимо заново пересоздавать объект ClaimsPrincipal и перезаписывать аутентификационные куки или jwt-токен, где эти данные храняться.
using Microsoft.AspNetCore.Authentication.Cookies; using System.Security.Claims; using Microsoft.AspNetCore.Authentication; var builder = WebApplication.CreateBuilder(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(); var app = builder.Build(); app.UseAuthentication(); // Добавление возраста app.MapGet("/addage", async (HttpContext context) => { if(context.User.Identity is ClaimsIdentity claimsIdentity) { claimsIdentity.AddClaim(new Claim("age", "37")); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); await context.SignInAsync(claimsPrincipal); } return Results.Redirect("/"); }); // удаление телефона app.MapGet("/removephone", async (HttpContext context) => { if (context.User.Identity is ClaimsIdentity claimsIdentity) { var phoneClaim = claimsIdentity.FindFirst(ClaimTypes.MobilePhone); // если claim успешно удален if(claimsIdentity.TryRemoveClaim(phoneClaim)) { var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); await context.SignInAsync(claimsPrincipal); } } return Results.Redirect("/"); }); app.MapGet("/login", async (HttpContext context) => { var username = "Tom"; var company = "Microsoft"; var phone = "+12345678901"; var claims = new List<Claim> { new Claim (ClaimTypes.Name, username), new Claim ("company", company), new Claim(ClaimTypes.MobilePhone,phone) }; var claimsIdentity = new ClaimsIdentity(claims, "Cookies"); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); await context.SignInAsync(claimsPrincipal); return Results.Redirect("/"); }); app.Map("/", (HttpContext context) => { var username = context.User.FindFirst(ClaimTypes.Name); var phone = context.User.FindFirst(ClaimTypes.MobilePhone); var company = context.User.FindFirst("company"); var age = context.User.FindFirst("age"); return $"Name: {username?.Value}\nPhone: {phone?.Value}\n" + $"Company: {company?.Value}\nAge: {age?.Value}"; }); app.MapGet("/logout", async (HttpContext context) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return "Данные удалены"; }); app.Run();
В данном случае конечная точка app.MapGet("/removephone")
удаляет телефон, а app.MapGet("/addage")
добавляет возвраст в claims
Если нам надо сохранить набор значений, то все они передаются по одному типу. Затем с помощью метода FindAll()
можно получить список этих значений.
Например, сохраним для пользователя набор иностранных языков:
using Microsoft.AspNetCore.Authentication.Cookies; using System.Security.Claims; using Microsoft.AspNetCore.Authentication; var builder = WebApplication.CreateBuilder(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(); var app = builder.Build(); app.UseAuthentication(); app.MapGet("/login", async (HttpContext context) => { var claims = new List<Claim> { new Claim (ClaimTypes.Name, "Tom"), new Claim ("languages", "English"), new Claim ("languages", "German"), new Claim ("languages", "Spanish") }; var claimsIdentity = new ClaimsIdentity(claims, "Cookies"); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); await context.SignInAsync(claimsPrincipal); return Results.Redirect("/"); }); app.Map("/", (HttpContext context) => { var username = context.User.FindFirst(ClaimTypes.Name); var languages = context.User.FindAll("languages"); // объединяем список claims в строку var languagesToString = ""; foreach (var l in languages) languagesToString = $"{languagesToString} {l.Value}"; return $"Name: {username?.Value}\nLanguages: {languagesToString}"; }); app.MapGet("/logout", async (HttpContext context) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return "Данные удалены"; }); app.Run();