ASP.NET Core | Авторизация по ролям

Asp.net core | jwt-токены

Последнее обновление: 27.12.2022

Общие подходы к авторизации и аутентификации в ASP.NET Core Web API несколько отличаются от того, что мы имеем в MVC. В частности, в Web API механизм авторизации полагается преимущественно
на JWT-токены. Что такое JWT-токен?

JWT (или JSON Web Token) представляет собой веб-стандарт, который определяет способ передачи данных о пользователе в формате JSON в зашифрованном виде.

JWT-токен состоит из трех частей:

Для использования JWT-токенов создадим новый проект ASP.NET Core по типу Empty.

JWT Token in ASP.NET Core Web API

Далее добавим в проект папку Models, в которой определим новый класс Person:

public class Person
{ public string Login { get; set; } public string Password { get; set; } public string Role { get; set; }
}

Этот класс будет описывать учетные записи пользователей в приложении.

Для работы с JWT-токенами установим через Nuget пакет Microsoft.AspNetCore.Authentication.JwtBearer.

Также добавим в проект специальный класс AuthOptions, который будет описывать ряд свойств, нужных для генерации токена:

using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace TokenApp
{ public class AuthOptions { public const string ISSUER = "MyAuthServer"; // издатель токена public const string AUDIENCE = "MyAuthClient"; // потребитель токена const string KEY = "mysupersecret_secretkey!123"; // ключ для шифрации public const int LIFETIME = 1; // время жизни токена - 1 минута public static SymmetricSecurityKey GetSymmetricSecurityKey() { return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KEY)); } }
}

Константа ISSUER представляет издателя токена. Здесь можно определить любое название. AUDIENCE представляет потребителя токена — опять же может быть любая строка,
но в данном случае указан адрес текущего приложения.

Константа KEY хранит ключ, который будет применяться для создания токена.

Для встраивания функциональности JWT-токенов в конвейер обработки запроса используется компонент JwtBearerAuthenticationMiddleware.
Так, изменим класс Startup следующим образом:

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
namespace TokenApp
{ public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { // укзывает, будет ли валидироваться издатель при валидации токена ValidateIssuer = true, // строка, представляющая издателя ValidIssuer = AuthOptions.ISSUER, // будет ли валидироваться потребитель токена ValidateAudience = true, // установка потребителя токена ValidAudience = AuthOptions.AUDIENCE, // будет ли валидироваться время существования ValidateLifetime = true, // установка ключа безопасности IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(), // валидация ключа безопасности ValidateIssuerSigningKey = true, }; }); services.AddControllersWithViews(); } public void Configure(IApplicationBuilder app) { app.UseDeveloperExceptionPage(); app.UseDefaultFiles(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); }); } }
}

Для установки аутентификации с помощью токенов в методе ConfigureServices в вызов services.AddAuthentication передается значение JwtBearerDefaults.AuthenticationScheme. Далее с помощью метода AddJwtBearer() добавляется конфигурация токена.

Для конфигурации токена применяется объект JwtBearerOptions, который позволяет с помощью свойств настроить работу с токеном. В данном случае использованы следующие свойства:

  • RequireHttpsMetadata: если равно false, то SSL при отправке токена не используется. Однако данный вариант установлен
    только дя тестирования. В реальном приложении все же лучше использовать передачу данных по протоколу https.

  • TokenValidationParameters: параметры валидации токена — сложный объект, определяющий, как токен будет валидироваться. Этот объект
    в свою очередь имеет множество свойств, которые позволяют настроить различные аспекты валидации токена. Но наиболее важные свойства:
    IssuerSigningKey — ключ безопасности, которым подписывается токен, и ValidateIssuerSigningKey — надо ли валидировать ключ безопасности.
    Ну и кроме того, можно установить ряд других свойств, таких как нужно ли валидировать издателя и потребителя токена, срок жизни токена, можно установить
    название claims для ролей и логинов пользователя и т.д.

Теперь мы можем использовать авторизацию на основе токенов. Однако в прокте пока не предусмотрена генерация токенов. По умолчанию в ASP.NET Core отсутствуют встроенные
возможности для создания токена. И в данном случае мы можем либо воспользоваться сторонними решениями (например,
IdentityServer или
OpenIdDict), либо же создать свой механизм. Выберем второй способ.

Создадим в проекте новую папку Controllers и добавим в нее новый контроллер AccountController:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using TokenApp.Models; // класс Person
namespace TokenApp.Controllers
{ public class AccountController : Controller { // тестовые данные вместо использования базы данных private List<Person> people = new List<Person> { new Person {Login="admin@gmail.com", Password="12345", Role = "admin" }, new Person { Login="qwerty@gmail.com", Password="55555", Role = "user" } }; [HttpPost("/token")] public IActionResult Token(string username, string password) { var identity = GetIdentity(username, password); if (identity == null) { return BadRequest(new { errorText = "Invalid username or password." }); } var now = DateTime.UtcNow; // создаем JWT-токен var jwt = new JwtSecurityToken( issuer: AuthOptions.ISSUER, audience: AuthOptions.AUDIENCE, notBefore: now, claims: identity.Claims, expires: now.Add(TimeSpan.FromMinutes(AuthOptions.LIFETIME)), signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256)); var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); var response = new { access_token = encodedJwt, username = identity.Name }; return Json(response); } private ClaimsIdentity GetIdentity(string username, string password) { Person person = people.FirstOrDefault(x => x.Login == username && x.Password == password); if (person != null) { var claims = new List<Claim> { new Claim(ClaimsIdentity.DefaultNameClaimType, person.Login), new Claim(ClaimsIdentity.DefaultRoleClaimType, person.Role) }; ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Token", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType); return claimsIdentity; } // если пользователя не найдено return null; } }
}

Для упрощения ситуации данные пользователей определены в виде простого списка. Для поиска пользователя в этом списке по логину и паролю определен метод
GetIdentity(), который возвращает объект ClaimsIdentity.

Принцип создания ClaimsIdentity здесь тот же, что и при аутентификации с помощью кук: создается набор объектов Claim, которые включают различные данные о пользователе,
например, логин, роль и т.д.

Для обработки запроса в контроллере создан метод Token, который сопоставлен с маршрутом «/token». Этот метод обрабатывает запросы POST и через
параметры принимает логин и пароль пользователя.

Сам токен представляет объект JwtSecurityToken, для инициализации которого применяются все те же константы и ключ безопасности,
которые определены в классе AuthOptions и которые использовались в классе Startup для настройки JwtBearerAuthenticationMiddleware. Важно, чтобы эти значения совпадали.

С помощью параметра claims: identity.Claims в токен добавляются набор объектов Claim, которые содержат информацию о логине и роли пользователя.

Далее посредством метода JwtSecurityTokenHandler().WriteToken(jwt) создается Json-представление токена. И в конце он сериализуется и отправляет клиенту с помощью метода Json().

Таким образом генерируется токен.

Для тестирования токена создадим простенький контроллер ValuesController:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
namespace TokenApp.Controllers
{	[ApiController] [Route("api/[controller]")] public class ValuesController : Controller { [Authorize] [Route("getlogin")] public IActionResult GetLogin() { return Ok($"Ваш логин: {User.Identity.Name}"); } [Authorize(Roles = "admin")] [Route("getrole")] public IActionResult GetRole() { return Ok("Ваша роль: администратор"); } }
}

И в конце добавим в проект для статических файлов папку wwwroot, а в нее — новый файл index.html:

<!DOCTYPE html>
<html>
<head> <meta charset="utf-8" /> <title>JWT в ASP.NET Core Web API</title>
</head>
<body> <div id="userInfo" style="display:none;"> <p>Вы вошли как: <span id="userName"></span></p> <input type="button" value="Выйти" id="logOut" /> </div> <div id="loginForm"> <h3>Вход на сайт</h3> <label>Введите email</label><br /> <input type="email" id="emailLogin" /> <br /><br /> <label>Введите пароль</label><br /> <input type="password" id="passwordLogin" /><br /><br /> <input type="submit" id="submitLogin" value="Логин" /> </div> <div> <input type="submit" id="getDataByLogin" value="Данные по логину" /> </div> <div> <input type="submit" id="getDataByRole" value="Данные по роли" /> </div> <script> var tokenKey = "accessToken"; // отпавка запроса к контроллеру AccountController для получения токена async function getTokenAsync() { // получаем данные формы и фомируем объект для отправки const formData = new FormData(); formData.append("grant_type", "password"); formData.append("username", document.getElementById("emailLogin").value); formData.append("password", document.getElementById("passwordLogin").value); // отправляет запрос и получаем ответ const response = await fetch("/token", { method: "POST", headers: {"Accept": "application/json"}, body: formData }); // получаем данные const data = await response.json(); // если запрос прошел нормально if (response.ok === true) { // изменяем содержимое и видимость блоков на странице document.getElementById("userName").innerText = data.username; document.getElementById("userInfo").style.display = "block"; document.getElementById("loginForm").style.display = "none"; // сохраняем в хранилище sessionStorage токен доступа sessionStorage.setItem(tokenKey, data.access_token); console.log(data.access_token); } else { // если произошла ошибка, из errorText получаем текст ошибки console.log("Error: ", response.status, data.errorText); } }; // отправка запроса к контроллеру ValuesController async function getData(url) { const token = sessionStorage.getItem(tokenKey); const response = await fetch(url, { method: "GET", headers: { "Accept": "application/json", "Authorization": "Bearer " token // передача токена в заголовке } }); if (response.ok === true) { const data = await response.json(); alert(data) } else console.log("Status: ", response.status); }; // получаем токен document.getElementById("submitLogin").addEventListener("click", e => { e.preventDefault(); getTokenAsync(); }); // условный выход - просто удаляем токен и меняем видимость блоков document.getElementById("logOut").addEventListener("click", e => { e.preventDefault(); document.getElementById("userName").innerText = ""; document.getElementById("userInfo").style.display = "none"; document.getElementById("loginForm").style.display = "block"; sessionStorage.removeItem(tokenKey); }); // кнопка получения имя пользователя - /api/values/getlogin document.getElementById("getDataByLogin").addEventListener("click", e => { e.preventDefault(); getData("/api/values/getlogin"); }); // кнопка получения роли - /api/values/getrole document.getElementById("getDataByRole").addEventListener("click", e => { e.preventDefault(); getData("/api/values/getrole"); }); </script>
</body>
</html>

Первый блок на странице выводит информацию о вошедшем пользователе и ссылку для выхода. Второй блок содержит форму для логина.

После нажатия кнопки на форме логина запрос будет отправляться методом POST на адрес «/token». Поскольку за обработку запросов по этому маршруту
отвечает метод Token контроллера AccountController, по результатам работы которого будет формироваться токен.

Ответом сервера в случае удачной аутентификации будет примерно следующий объект:

{	access_token : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93	cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoicXdlcnR5IiwiaHR0cDovL3NjaGVtYXMub	Wljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoidXNlciIsIm5iZi	I6MTQ4MTYzOTMxMSwiZXhwIjoxNDgxNjM5MzcxLCJpc3MiOiJNeUF1dGhTZXJ2ZXIiLCJhdWQiOiJ	odHRwOi8vbG9jYWxob3N0OjUxODg0LyJ9.dQJF6pALUZW3wGBANy_tCwk5_NR0TVBwgnxRbblp5Ho",	username: "qwerty@gmail.com"
}

Параметр access_token как раз и будет представлять токен доступа. Также в объекте передается дополнительная информация о нике
пользователя.

Для того, чтобы в коде js данный токен в дальнейшем был доступен, то он сохраняется в хранилище sessionStorage.

Последние два блока предназначены для отправки запросов к методам ValuesController. Чтобы отправить токен в запросе, нам нужно настроить в запросе заголовок Authorization:

headers: {	"Accept": "application/json",	"Authorization": "Bearer " token // передача токена в заголовке
}

В итоге весь проект будет выглядеть следующим образом:

JWT в ASP.NET Core Web API

По ранее сохраненному ключу получаем из хранилища sessionStorage токен и формируем заголовок.

Теперь после получения токена мы можем осуществить запрос к методам контроллера ValuesController:

Роли в токене JWT в ASP.NET Core Web API

В то же время если мы попробуем обратиться к тем же методам без токена или с токеном с истекшим сроком, то получим ошибку 401 (Unauthorized).

НазадСодержаниеВперед

Похожее:  Авторизация и аутентификация squid (basic, Digest, NTLM, negotiate, Kerberos), SQUID AD, squid auth | Блог любителя экспериментов