Содержание
До этого момента мы разбирались с аутентификацией и авторизацией пользователей в ASP.NET Core с использованием JWT-токенов. При этом, не менее популярным вариантом аутентификации пользователей не только в ASP.NET Core, но и, в принципе, в веб-приложениях, является аутентификация с помощью cookies (куки). Рассмотрим этот вариант аутентификации, разработав небольшое приложение ASP.NET
Подключение и настройка необходимых сервисов и компонентов middleware
Создадим новое приложение ASP.NET Core с шаблоном Empty и сразу настроим необходимые элементы аутентификации и авторизации:
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; namespace AspCookieAuth { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); //подключаем аутентификацию со схемой Cookies builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/login"; options.Cookie.Name = "authCookie"; }); //подключаем серсив авторизации builder.Services.AddAuthorization(); var app = builder.Build(); //подключаем необходимые компоненты middleware app.UseAuthentication(); app.UseAuthorization(); app.MapGet("/", [Authorize]() => "Hello World!"); app.Run(); } } }
при подключении аутентификации мы указали схему Cookies, а также вызвали метод AddCookie
в котором настроили аутентификацию, а именно:
options.LoginPath = "/login";
указывает путь по которому будет перенаправлен пользователь при попытку получить доступ к защищенному ресурсу:
options.Cookie.Name = "authCookie";
имя аутентификационной куки.
У конечной точки «/» мы указали атрибут Authorize
. Теперь, если мы запустим приложение, то увидим вот такое сообщение об ошибке:
Так как страницы «/login
» у нас ещё нет. Создадим её.
Создание необходимых конечных точек
app.MapGet("/login", async (HttpContext context) => { context.Response.ContentType = "text/html; charset=utf-8"; // html-форма для ввода логина/пароля await context.Response.SendFileAsync(@"wwwroot\login.html"); }); app.MapPost("/login", async (string? returnUrl, HttpContext context) => { // получаем из формы логин и пароль var form = context.Request.Form; // если email и/или пароль не установлены, посылаем статусный код ошибки 400 if (!form.ContainsKey("login") || !form.ContainsKey("password")) return Results.BadRequest("Логин и/или пароль не установлены"); string login = form["login"]; string password = form["password"]; // находим пользователя User? user = users.FirstOrDefault(p => p.Login == login && p.Password == password); // если пользователь не найден, отправляем статусный код 401 if (user is null) return Results.Unauthorized(); var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.Login) }; // создаем объект ClaimsIdentity ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); // установка аутентификационных куки await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); return Results.Redirect(returnUrl ?? "/"); });
Если пользователь выполняет GET-запрос на адрес "/login"
, то срабатывает обработчик, который отправляет пользователю содержимое файла "login.html"
, который был создан в папке wwwroot:
и имеет следующее содержимое:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Вход в систему</title> </head> <body> <form method='post'> <p> <label>Login</label><br /> <input name='login' /> </p> <p> <label>Password</label><br /> <input type='password' name='password' /> </p> <input type='submit' value='Login' /> </form> </body> </html>
Это обычная web-форма для ввода логина и пароля пользователя. При нажатии на кнопку «Login» выполняется POST-запрос, то есть срабатывает следующая конечная точка:
app.MapPost("/login", async (string? returnUrl, HttpContext context) => { // получаем из формы логин и пароль var form = context.Request.Form; if (!form.ContainsKey("login") || !form.ContainsKey("password")) return Results.BadRequest("Логин и/или пароль не установлены"); string login = form["login"]; string password = form["password"]; // находим пользователя User? user = users.FirstOrDefault(p => p.Login == login && p.Password == password); // если пользователь не найден, отправляем статусный код 401 if (user is null) return Results.Unauthorized(); var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.Login) }; // создаем объект ClaimsIdentity ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); // установка аутентификационных куки await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); return Results.Redirect(returnUrl ?? "/"); });
здесь мы выполняем следующие действия:
Получаем доступ к содержимому формы:
// получаем из формы логин и пароль var form = context.Request.Form; if (!form.ContainsKey("login") || !form.ContainsKey("password")) return Results.BadRequest("Логин и/или пароль не установлены"); string login = form["login"]; string password = form["password"];
если в форме нет логина или пароля пользователя, то отправляем пользователю ошибку BadRequest, используя статический класс Results
. Если форма заполнена корректно, то считываем логин и пароль в локальные переменные.
Ищем пользователя в списке users
.
// находим пользователя User? user = users.FirstOrDefault(p => p.Login == login && p.Password == password); // если пользователь не найден, отправляем статусный код 401 if (user is null) return Results.Unauthorized();
В приложении этот список выглядит следующим образом:
public class User { public string Login { get; set; } public string Password { get; set; } } var users = new List<User>() { new User(){Login = "admin", Password = "root"}, new User(){Login = "tom", Password = "user"}, };
Далее мы создаем объект ClaimIdentity
со списком объектов Claim
в который добавляем только имя пользователя
var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.Login) }; // создаем объект ClaimsIdentity ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
с классом ClaimIdentity
мы уже знакомились здесь.
Далее мы устанавливаем аутентификационные куки:
await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
здесь мы используем один из методов расширения объекта HttpContext
— SignInAsync
, который принимает в качестве параметров схему аутентификации и объект типа ClaimsPrincipal
.
После установки куки отправляем пользователя снова на главную страницу приложения:
return Results.Redirect(returnUrl ?? "/");
Для выхода из приложения предусмотрим следующую конечную точку
app.MapGet("/logout", async (HttpContext context) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return Results.Redirect("/login"); });
Посмотрим на работу приложения.
Проверка работы приложения
Запускаем приложение и автоматически переходим на форму аутентификации:
Вводим логин и пароль пользователя и жмем кнопку Login:
В инструментах разработчика в браузере можно убедиться, что кука была установлена:
Переходим по пути «/logout» и убеждаемся, что аутентификационная кука убирается:
Приложение работает так как и задумывалось. Приведем весь исходный код приложения
Исходный код приложения
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using System.Security.Claims; namespace AspCookieAuth { public class User { public string Login { get; set; } public string Password { get; set; } } public class Program { public static void Main(string[] args) { var users = new List<User>() { new User(){Login = "admin", Password = "root"}, new User(){Login = "tom", Password = "user"}, }; var builder = WebApplication.CreateBuilder(args); //подключаем аутентификацию со схемой Cookies builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/login"; options.Cookie.Name = "authCookie"; }); //подключаем серсив авторизации builder.Services.AddAuthorization(); var app = builder.Build(); //app.UseStaticFiles(); //подключаем необходимые компоненты middleware app.UseAuthentication(); app.UseAuthorization(); app.MapGet("/", [Authorize] () => "Hello World!"); app.MapGet("/login", async (HttpContext context) => { context.Response.ContentType = "text/html; charset=utf-8"; // html-форма для ввода логина/пароля await context.Response.SendFileAsync(@"wwwroot\login.html"); }); app.MapPost("/login", async (string? returnUrl, HttpContext context) => { // получаем из формы логин и пароль var form = context.Request.Form; if (!form.ContainsKey("login") || !form.ContainsKey("password")) return Results.BadRequest("Логин и/или пароль не установлены"); string login = form["login"]; string password = form["password"]; // находим пользователя User? user = users.FirstOrDefault(p => p.Login == login && p.Password == password); // если пользователь не найден, отправляем статусный код 401 if (user is null) return Results.Unauthorized(); var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.Login) }; // создаем объект ClaimsIdentity ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); // установка аутентификационных куки await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); return Results.Redirect(returnUrl ?? "/"); }); app.MapGet("/logout", async (HttpContext context) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return Results.Redirect("/login"); }); app.Run(); } } }
Итого
Аутентификация на основе cookie используется в ASP.NET Core наряду с другими схемами аутентификации и, по сравнению с аутентификацией по JWT-токенам выглядит более простой. Тем не менее такой подход к аутентификации пользователей широко используется в веб-приложениях. Что касается использования авторизации по ролям или с использованием политик, то здесь используются те же механизмы, которые мы рассматривали ранее.