Содержание
До этого момента мы разбирались с аутентификацией и авторизацией пользователей в 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-токенам выглядит более простой. Тем не менее такой подход к аутентификации пользователей широко используется в веб-приложениях. Что касается использования авторизации по ролям или с использованием политик, то здесь используются те же механизмы, которые мы рассматривали ранее.




