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.
Далее добавим в проект папку 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="[email protected]", Password="12345", Role = "admin" }, new Person { Login="[email protected]", 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: "[email protected]" }
Параметр access_token
как раз и будет представлять токен доступа. Также в объекте передается дополнительная информация о нике
пользователя.
Для того, чтобы в коде js данный токен в дальнейшем был доступен, то он сохраняется в хранилище sessionStorage.
Последние два блока предназначены для отправки запросов к методам ValuesController. Чтобы отправить токен в запросе, нам нужно настроить в запросе заголовок Authorization:
headers: { "Accept": "application/json", "Authorization": "Bearer " token // передача токена в заголовке }
В итоге весь проект будет выглядеть следующим образом:
По ранее сохраненному ключу получаем из хранилища sessionStorage токен и формируем заголовок.
Теперь после получения токена мы можем осуществить запрос к методам контроллера ValuesController:
В то же время если мы попробуем обратиться к тем же методам без токена или с токеном с истекшим сроком, то получим ошибку 401 (Unauthorized).
НазадСодержаниеВперед