Что дальше?
Подумаем о безопасности и добавим Refresh Token. Смотрите следующую мою статью на эту тему.
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="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 // передача токена в заголовке
}В итоге весь проект будет выглядеть следующим образом:

По ранее сохраненному ключу получаем из хранилища sessionStorage токен и формируем заголовок.
Теперь после получения токена мы можем осуществить запрос к методам контроллера ValuesController:

В то же время если мы попробуем обратиться к тем же методам без токена или с токеном с истекшим сроком, то получим ошибку 401 (Unauthorized).
НазадСодержаниеВперед
Jwt аутентификация
Необходимые инструменты:
Visual Studio 2022 — Можно загрузить здесь
.Net 5.0 SDK — Можно загрузить здесь
План работы:
- Настройка проекта веб-API .NET 5.0.
- Настройка аутентификацию JWT
- Генерация токена JWT.
- Валидация токена JWT, используя кастомное промежуточное ПО и атрибут авторизации.
- Тестирование API с использованием Swagger.
Генерация jwt токена
Давайте создадим контроллер с именем AuthController внутри папки controllers, и добавим метод Auth, который отвечает для проверку учетных данных входа и создадим токен на основе имени пользователя. Мы отметили этот метод с помощью атрибута AllowAnonymous для обхода аутентификации. Этот метод ожидает объект LoginModel для имени пользователя и пароля.
Мы создали директорию Services, в которой находится наша основная бизнес-логика, и мы используем внедрение зависимостей для использования этих служб в контроллере.
В демонстрационных целях я жестко запрограммировал значения внутри метода для проверки модели.
Генерация токена
Чтобы пользователь мог использовать токен, приложение должно отправить ему этот токен, а перед этим соответственно сгенерировать токен.
И для генерации токена здесь предусмотрена типовая конечная точка «/login»:
Использование алгоритма хеширования none
Подобная атака происходит, когда злоумышленник изменяет токен, а также меняет алгоритм хеширования (поле “alg”), чтобы указать через ключевое слово none, что целостность токена уже проверена. Некоторые библиотеки рассматривали токены, подписанные с помощью алгоритма none, как действительный токен с проверенной подписью, поэтому злоумышленник мог изменить полезную нагрузку (payload) токена, и приложение доверяло бы токену.
Для предотвращения атаки необходимо использовать библиотеку JWT, которая не подвержена данной уязвимости. Также во время проверки валидности токена необходимо явно запросить использование ожидаемого алгоритма.
Пример реализации:
// Ключ HMAC хранится как String в памяти JVM
private transient byte[] keyHMAC = ...;
...
// Создание контекста верификации для запроса к токену
// Явно указывается использование HMAC-256 хеш-алгоритма
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)).build();
// Верификация токена
DecodedJWT decodedToken = verifier.verify(token);Использование слабого ключа при создании токена
Если секрет, используемый в случае алгоритма HMAC-SHA256, необходимый для подписи токена, является слабым, то он может быть взломан (подобран c помощью атаки грубой силы). В результате злоумышленник может подделать произвольный действительный токен с точки зрения подписи.
Для предотвращения этой проблемы надо использовать сложный секретный ключ: буквенно-цифровой (смешанный регистр) специальные символы.
Поскольку ключ необходим только для компьютерных вычислений, размер секретного ключа может превышать 50 позиций.
Например:
A&'/}Z57M(2hNg=;LE?~]YtRMS5(yZ<vcZTA3N-($>2j:ZeX-BGftaVk`)jKP~q?,jk)EMbgt*kW'Для оценки сложности секретного ключа, используемого для вашей подписи токена, вы можете применить атаку по словарю паролей к токену в сочетании с JWT API.
Исходный код
Полный исходный код рассмотренного приложения находится на
Конфигурация и валидация токена
С помощью метода AddJwtBearer() в приложение добавляется конфигурация токена. Для конфигурации токена применяется
объект JwtBearerOptions, который позволяет с помощью свойств настроить работу с токеном.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = AuthOptions.ISSUER, ValidateAudience = true, ValidAudience = AuthOptions.AUDIENCE, ValidateLifetime = true, IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(), ValidateIssuerSigningKey = true, };
});Объект TokenValidationParameters обладает множеством свойств, которые позволяют настроить различные аспекты валидации токена. В данном случае применяются следующие свойства:
Настроим spring security
Соберем все проделанное выше вместе и настроим Spring Security.
Настройка jwt-аутентификации
Чтобы настроить JWT (JSON веб-токен), у нас должен быть установлен пакет Nuget внутри проекта, поэтому давайте сначала добавим зависимости проекта.
Пакеты NuGet для установки
Внутри Visual Studio — нажмите на Tools -> Nuget Package Manager -> Manage Nuget packages for solution.
Установить через консоль.
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 5.0.7Первый шаг — настроить аутентификацию JWT в нашем проекте. Для этого нам нужно зарегистрировать схему JWT в Swagger Service, используя метод Addauthentication.
Давайте определим службу SWARGER и закрепим за ней авторизацию по JWT.
#region Swagger Configuration services.AddSwaggerGen(swagger => { //This is to generate the Default UI of Swagger Documentation swagger.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "JWT Token Authentication API", Description = "ASP.NET Core 5.0 Web API" }); // To Enable authorization using Swagger (JWT) swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() { Name = "Authorization", Type = SecuritySchemeType.ApiKey, Scheme = "Bearer", BearerFormat = "JWT", In = ParameterLocation.Header, Description = "JWT Authorization header using the Bearer scheme. rnrn Enter 'Bearer' [space] and then your token in the text input below.rnrnExample: "Bearer 12345abcdef"", }); swagger.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] {} } }); }); #endregionДобавим службу для выполнения аутентификации, а также вызовем AddJWTBearer для конфигурации авторизации.
#region Authentication services.AddAuthentication(option => { option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = false, ValidateIssuerSigningKey = true, ValidIssuer = Configuration["Jwt:Issuer"], ValidAudience = Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])) //Configuration["JwtToken:SecretKey"] }; }); #endregionВ приведенном выше примере мы указали параметры, которые необходимо учитывать для проверки токена.
- Проверка на сервере (
Validateissuer = true), который генерирует токен. - Проверка получателя токена, авторизован ли он для получения токена (
ValidateAudience = True) - Проверка, не истек ли токен и валиден ли ключ подписания эмитента (
ValidateLifetime = True) - Проверка подписи токена (
ValidateissuerSigningKey = True)
Мы должны указать значения для Audience, Issuer и Secret key в этом проекте, мы сохранили эти значения внутри файла appsettings.json.
appsettings.json
Настройка проект .net 5.0 web api
Откройте Visual Studio, выберите «Создать новый проект» и нажмите кнопку «Далее».
Добавьте «Имя проекта» и «Имя решения» также выберите путь, чтобы сохранить проект в этом месте, нажмите «Далее».
Теперь выберите целевой фреймворк .NET 5.0, что мы получаем, как только мы устанавливаем SDK, а также получим еще один вариант для настройки поддержки OpenAPI по умолчанию с помощью этого флажка.
Немного теории
Аутентификация
Переопределим authenticationfailurehandler
В случае, если пользователь не прошел аутентификацию, мы перенаправим его по адресу обратного вызова, который был передан в начале процесса аутентификации с параметром error, содержащим текст ошибки.
public class ExampleAuthenticationFailureHandler implements AuthenticationFailureHandler { private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository; public ExampleAuthenticationFailureHandler( HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) { this.authorizationRequestRepository = requireNonNull(authorizationRequestRepository); } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { String targetUrl = getFailureUrl(request, exception); authorizationRequestRepository.removeAuthorizationRequestCookies(request, response); redirectStrategy.sendRedirect(request, response, targetUrl); } private String getFailureUrl(HttpServletRequest request, AuthenticationException exception) { String targetUrl = getCookie(request, Cookies.REDIRECT_URI) .map(Cookie::getValue) .orElse(("/")); return UriComponentsBuilder.fromUriString(targetUrl) .queryParam("error", exception.getLocalizedMessage()) .build().toUriString(); }
}Перехват токенов
Атака происходит, когда токен был перехвачен или украден злоумышленником и он применяет его для получения доступа к системе, используя идентификационные данные определенного пользователя.
Защита заключается в добавлении «пользовательского контекста» в токен. Пользовательский контекст будет состоять из следующей информации:
Полезные ссылки
- 5 Easy Steps to Understanding JSON Web Tokens (JWT)
- Securing React Redux Apps With JWT Tokens
- Зачем нужен Refresh Token, если есть Access Token?
Раскрытие информации о токене
Эта атака происходит, когда злоумышленник получает доступ к токену (или к набору токенов) и извлекает сохраненную в нем информацию (информация о токене JWT кодируется с помощью base64) для получения информации о системе. Информация может быть, например, такой как, роли безопасности, формат входа в систему и т.д.
Способ защиты достаточно очевиден и заключается в шифровании токена. Также важно защитить зашифрованные данные от атак с использованием криптоанализа. Для достижения всех этих целей используется алгоритм AES-GCM, который обеспечивает аутентифицированное шифрование с ассоциированными данными (Authenticated Encryption with Associated Data – AEAD).
Примитив AEAD обеспечивает функциональность симметричного аутентифицированного шифрования. Реализации этого примитива защищены от адаптивных атак на основе подобранного шифртекста. При шифровании открытого текста можно дополнительно указать связанные данные, которые должны быть аутентифицированы, но не зашифрованы.
То есть шифрование с соответствующими данными обеспечивает подлинность и целостность данных, но не их секретность.
Однако необходимо отметить, что шифрование добавляется в основном для сокрытия внутренней информации, но очень важно помнить, что первоначальной защитой от подделки токена JWT является подпись, поэтому подпись токена и ее проверка должны быть всегда использованы.
Реализация
Мы реализуем REST-сервис, предоставляющий следующее API:
Высокоуровневая архитектура приложения
Заметим, что поскольку приложение состоит из трех взаимодействующих компонентов, помимо того, что мы выполняем авторизацию запросов клиента к серверу, Bitbucket авторизует запросы сервера к нему. Мы не будем настраивать авторизацию методов по ролям, чтобы не делать пример сложнее.

Процесс регистрации OAuth клиента описан в предыдущей статье
Для реализации мы будем использовать Spring Boot версии 2.2.2.RELEASE и Spring Security версии 5.2.1.RELEASE.
Создадим tokenauthenticationfilter
Задача этого фильтра извлечь токен доступа из заголовка Authorization в случае его наличия, провалидировать его и инициализировать секьюрити контекст.
Соображения по поводу использования jwt
Даже если токен JWT прост в использовании и позволяет предоставлять сервисы (в основном REST) без сохранения состояния (stateless), такое решение подходит не для всех приложений, потому что оно поставляется с некоторыми оговорками, как, например, вопрос хранения токена.
Если приложение не должно быть полностью stateless, то можно рассмотреть возможность использования традиционной системы сессий, предоставляемой всеми веб-платформами. Однако для stateless приложений JWT – это хороший вариант, если он правильно реализован.
Ссылки
P.S.
Структура jwt
JWT состоит из трех частей: заголовок header, полезные данные payload и подпись signature. Давайте пройдемся по каждой из них.
Хедер JWT содержит информацию о том, как должна вычисляться JWT подпись. Хедер — это тоже JSON объект, который выглядит следующим образом:
header = { "alg": "HS256", "typ": "JWT"}Поле typ не говорит нам ничего нового, только то, что это JSON Web Token. Интереснее здесь будет поле alg, которое определяет алгоритм хеширования. Он будет использоваться при создании подписи. HS256 — не что иное, как HMAC-SHA256, для его вычисления нужен лишь один секретный ключ (более подробно об этом в шаге 3).
Еще может использоваться другой алгоритм RS256 — в отличие от предыдущего, он является ассиметричным и создает два ключа: публичный и приватный. С помощью приватного ключа создается подпись, а с помощью публичного только лишь проверяется подлинность подписи, поэтому нам не нужно беспокоиться о его безопасности.
Тестирование api с помощью swagger (openapi)
Запустите приложение. Это приведет нас к странице Swagger Index со всей настроенной конфигурацией, которую мы сделали в проекте.
Давайте передадим действительные учетные данные Auth API, чтобы получить токен доступа.
Хранение токенов на стороне клиента
Если приложение хранит токен так, что возникает одна или несколько из следующих ситуаций:
Для предотвращения атаки:
- Хранить токен в браузере, используя контейнер sessionStorage.
- Добавить его в заголовок Authorization, используя схему Bearer. Заголовок должен выглядеть следующим образом:
Authorization: Bearer <token> - Добавить fingerprint информацию к токену.
Сохраняя токен в контейнере sessionStorage, он предоставляет токен для кражи в случае XSS. Однако fingerprint, добавленный в токен, предотвращает повторное использование украденного токена злоумышленником на его компьютере. Чтобы закрыть максимум областей использования для злоумышленника, добавьте Политику безопасности содержимого браузера (Content Security Policy), чтобы ограничить контекст выполнения.
Остается случай, когда злоумышленник использует контекст просмотра пользователя в качестве прокси-сервера, чтобы использовать целевое приложение через легитимного пользователя, но Content Security Policy может предотвратить связь с непредвиденными доменами.
Также возможно реализовать службу аутентификации таким образом, чтобы токен выдавался внутри защищенного файла cookie, но в этом случае должна быть реализована защита от CSRF.
Шаг 3. создаем signature
Подпись вычисляется с использование следующего псевдо-кода:
const SECRET_KEY = 'cAtwa1kkEy'
const unsignedToken = base64urlEncode(header) '.' base64urlEncode(payload)
const signature = HMAC-SHA256(unsignedToken, SECRET_KEY)Алгоритм base64url кодирует хедер и payload, созданные на 1 и 2 шаге. Алгоритм соединяет закодированные строки через точку. Затем полученная строка хешируется алгоритмом, заданным в хедере на основе нашего секретного ключа.
// header eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
// payload eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ
// signature -xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcMШаг 4. теперь объединим все три jwt компонента вместе
Теперь, когда у нас есть все три составляющих, мы можем создать наш JWT. Это довольно просто, мы соединяем все полученные элементы в строку через точку.
const token = encodeBase64Url(header) '.' encodeBase64Url(payload) '.' encodeBase64Url(signature)
// JWT Token
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcMВы можете попробовать создать свой собственный JWT на сайте jwt.io.Вернемся к нашему примеру. Теперь сервер аутентификации может слать пользователю JWT.
Явное аннулирование токена пользователем
Поскольку токен становится недействительным только после истечения срока его действия, у пользователя нет встроенной функции, позволяющей явно отменить действие токена. Таким образом, в случае кражи пользователь не может сам отозвать токен и затем заблокировать атакующего.
Одним из способов защиты является внедрение черного списка токенов, который будет пригоден для имитации функции «выход из системы», существующей в традиционной системе сеансов.
В черном списке будет храниться сборник (в кодировке SHA-256 в HEX) токена с датой аннулирования, которая должна превышать срок действия выданного токена.
Когда пользователь хочет «выйти», он вызывает специальную службу, которая добавляет предоставленный токен пользователя в черный список, что приводит к немедленному аннулированию токена для дальнейшего использования в приложении.
Пример реализации:
Хранилище черного списка:Для централизованного хранения черного списка будет использоваться база данных со следующей структурой:
create table if not exists revoked_token(jwt_token_digest varchar(255) primary key,
revokation_date timestamp default now());Управление аннулированиями токенов:
// Контролирование отката токена (logout).
// Используйте БД, чтобы разрешить нескольким экземплярам проверять
// отозванный токен и разрешить очистку на уровне централизованной БД.
public class TokenRevoker { // Подключение к БД @Resource("jdbc/storeDS") private DataSource storeDS; // Проверка является ли токен отозванным public boolean isTokenRevoked(String jwtInHex) throws Exception { boolean tokenIsPresent = false; if (jwtInHex != null && !jwtInHex.trim().isEmpty()) { // Декодирование токена byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex); // Вычисление SHA256 от токена MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] cipheredTokenDigest = digest.digest(cipheredToken); String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest); // Поиск токена в БД try (Connection con = this.storeDS.getConnection()) { String query = "select jwt_token_digest from revoked_token where jwt_token_digest = ?"; try (PreparedStatement pStatement = con.prepareStatement(query)) { pStatement.setString(1, jwtTokenDigestInHex); try (ResultSet rSet = pStatement.executeQuery()) { tokenIsPresent = rSet.next(); } } } } return tokenIsPresent; }
// Добавление закодированного в HEX токена в таблица отозванных токенов
public void revokeToken(String jwtInHex) throws Exception { if (jwtInHex != null && !jwtInHex.trim().isEmpty()) { // Декодирование токена byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex); // Вычисление SHA256 от токена MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] cipheredTokenDigest = digest.digest(cipheredToken); String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest); // Проверка на наличие токена уже в БД и занесение в БД в // обратном случае if (!this.isTokenRevoked(jwtInHex)) { try (Connection con = this.storeDS.getConnection()) { String query = "insert into revoked_token(jwt_token_digest) values(?)"; int insertedRecordCount; try (PreparedStatement pStatement = con.prepareStatement(query)) { pStatement.setString(1, jwtTokenDigestInHex); insertedRecordCount = pStatement.executeUpdate(); } if (insertedRecordCount != 1) { throw new IllegalStateException("Number of inserted record is invalid," " 1 expected but is " insertedRecordCount); } } } } }Создадим login endpoint
Для аутентификации пользователя мы по-прежнему используем OAuth2 с типом авторизации Authorization Code. Однако на предыдущем шаге мы заменили стандартный AuthenticationEntryPoint своей реализацией, поэтому нам нужен явный способ запустить процесс аутентификации.
Вход в личный кабинет