Содержание
В предыдущей части мы рассмотрели основные моменты, связанные с аутентификацией и авторизацией пользователя и подключили необходимые сервисы и компоненты middleware в проект. Сегодня рассмотрим процесс аутентификации пользователя на основе JWT-токенов.
Что такое JWT-токен? Как использовать JWT-токены в ASP.NET Core
JWT-токен (Json Web-Token) — это открытый стандарт (RFC 7519), который определяет компактный и автономный способ безопасной передачи информации между сторонами в виде объекта JSON.
JWT-токены состоят из трех частей:
- Header (заголовок). Заголовок обычно состоит из двух частей: типа токена и используемого алгоритма подписи, такого как HMAC SHA256 или RSA.
- Payload (полезная нагрузка). Здесь обычно содержатся некие утверждения, то есть информация о пользователе, которая может использоваться для авторизации.
- Signature (подпись) — это строка, которая создается на основании первых двух элементов с использованием алгоритма, указанного в заголовке токена.
Для обычного пользователя JWT-токен выглядит как три строки закодированные в Base64 и разделенные точками. Чтобы использовать JWT-токены в ASP.NET Core там потребуется установить в проект nuget-пакет под названием Microsoft.
Аутентификация на основе JWT-токенов
Чтобы аутентифицировать пользователя с использованием JWT-токена мы должны каким-либо образом провести проверку пользователя (например, запросить логин/пароль), затем сформировать для пользователя JWT-токен, отправить пользователю токен.
Создадим новый пустой проект ASP.NET Core, добавим в него nuget-пакет Microsoft.
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt;
Теперь необходимо настроить сервис аутентификации. Для настройки сервиса аутентификации с Jwt-токенами используется объект класса JwtBearerOptions
, который содержит достаточно большое количество различных настроек. Используем пока только некоторые из них. Вот как будет выглядеть часть метода Main()
с настройками аутентификации:
public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); var authOptions = builder.Configuration.GetSection("Jwt").Get<AuthOptions>(); SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authOptions.SecretKey)); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = authOptions.ValidateIssuer, ValidateAudience = authOptions.ValidateAudience, ValidateLifetime = authOptions.ValidateLifetime, ValidateIssuerSigningKey = authOptions.ValidateIssuerSigningKey, ValidIssuer = authOptions.Issuer, ValidAudience = authOptions.Audience, IssuerSigningKey = key }; }); builder.Services.AddAuthorization(); var app = builder.Build(); //остальной код метода
Вначале мы считываем настройки аутентификации из конфигурации приложения:
var authOptions = builder.Configuration.GetSection("Jwt").Get<AuthOptions>();
класс AuthOptions
выглядит следующим образом:
public class AuthOptions { public string Issuer { get; set; } //издатель токена public string Audience { get; set; } //потребитель токена public string SecretKey { get; set; } //секретный ключ для подписи public bool ValidateAudience { get; set; } //проверять потребителя public bool ValidateIssuer { get; set; } //проверять издателя public bool ValidateLifetime { get; set; } //проверять время жизни токена public bool ValidateIssuerSigningKey { get; set; } //проверять подпись public int TokenLifetime { get; set; } //время жизни токена в минутах }
при этом, файл appsettings.json выглядит следующим образом:
{ "Jwt": { "Issuer": "csharp.webdelphi.ru", "Audience": "csharp.webdelphi.ru", "SecretKey": "my_security_key_for_auth", "ValidateAudience": true, "ValidateLifetime": true, "ValidateIssuer": true, "ValidateIssuerSigningKey": true, "TokenLifetime": 2 }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }
После получения настроек из конфигурации мы создаем ключ для подписи токена:
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authOptions.SecretKey));
и далее мы указываем настройки проверки токена:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = authOptions.ValidateIssuer, ValidateAudience = authOptions.ValidateAudience, ValidateLifetime = authOptions.ValidateLifetime, ValidateIssuerSigningKey = authOptions.ValidateIssuerSigningKey, ValidIssuer = authOptions.Issuer, ValidAudience = authOptions.Audience, IssuerSigningKey = key }; });
Теперь мы должны проверить пользователя и выдать ему токен. Создадим новую конечную точку:
//условная база данных с учетными записями пользователей List<User> UserDatabase = new() { new User() { Login = "user", Password ="12345" }, new User() { Login = "admin", Password = "root" }, }; app.MapGet("/login", (string? login, string? password) => { if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password)) return Results.BadRequest(new { Error = "Не задан логин или пароль" }); var user = UserDatabase.FirstOrDefault(u => (u.Login == login) && (u.Password == password)); if (user == null) return Results.BadRequest(new { Error = "Не верно введен логин или пароль" }); JwtSecurityToken token = new( issuer: authOptions?.Issuer, audience: authOptions?.Audience, expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(authOptions.TokenLifetime)), signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256) ); JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); string tokenString = handler.WriteToken(token); return Results.Ok(new { token = tokenString, user = login }); });
Для простоты, логин и пароль пользователя передаются через параметры запроса.
Вначале проверяем наличие логина и пароля как таковых. Если логин или пароль не переданы, то отправляем пользователю ошибку 400.
Далее, мы используем нашу импровизированную БД и ищем пользователя с заданным логином и паролем:
var user = UserDatabase.FirstOrDefault(u => (u.Login == login) && (u.Password == password));
Если запись в списке не найдена, то снова возвращаем пользователю BadRequest. Если же пользователь обнаружен, то мы создаем объект токена, используя полученные ранее настройки:
JwtSecurityToken token = new( issuer: authOptions?.Issuer, audience: authOptions?.Audience, expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(authOptions.TokenLifetime)), signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256) );
Чтобы создать непосредственно сам JWT-токен используется объект класса JwtSecurityTokenHandler
у которого определен метод WriteToken
, который записывает токен в виде строки:
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); string tokenString = handler.WriteToken(token);
и, в заключении, мы отсылаем пользователю объект, содержащий логин и токен пользователя:
return Results.Ok(new { token = tokenString, user = login });
В запущенном приложении мы должны будем увидеть следующее:
В конце этой части будет приведен весь исходный код приложения, а пока нам необходимо проверить работу нашего токена.
Авторизация по JWT-токену
После того, как токен получен, пользователь должен отправлять его на сервер каждый раз при попытке получить доступ к ресурсу, требующему авторизации. Токен отправляется в заголовке:
"Authorization: Bearer " + token
Создадим новую конечную точку:
app.MapGet("/auth", [Authorize] () => { return Results.Ok(authOptions); });
для примера, если авторизация пользователя проходит успешно, то пользователю вернутся настройки аутентификации, которые мы ранее записывали в файл appsettings.json.
Для проверки токена мы можем воспользоваться любым онлайн-сервисом для тестирования REST API, например, https://reqbin.com/
. Снова получаем токен, заходим на ReqBin и вводим его в поле Token
В поле адреса вносим адрес на который необходимо выполнить запрос. В моем случае — это https://localhost:7137/auth и жмем кнопку «Send». Сервер вернет нам ответ:
Если мы попробуем зайти на этот же адрес через 2-3 минуты, то получим ошибку:
так как ранее мы установили в настройках время жизни токена 2 минуты. Как видно, токен «работает». Ниже приведен весь исходный код проекта
Исходный код проекта, реализующего аутентификацию пользователя с помощью JWT-токена
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Text; namespace AspAuth { public class AuthOptions { public string Issuer { get; set; } //издатель токена public string Audience { get; set; } //потребитель токена public string SecretKey { get; set; } //секретный ключ для подписи public bool ValidateAudience { get; set; } //проверять потребителя public bool ValidateIssuer { get; set; } //проверять издателя public bool ValidateLifetime { get; set; } //проверять время жизни токена public bool ValidateIssuerSigningKey { get; set; } //проверять подпись public int TokenLifetime { get; set; } //время жизни токена в минутах } public class User { public string Login { get; set; } public string Password { get; set; } } public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); var authOptions = builder.Configuration.GetSection("Jwt").Get<AuthOptions>(); SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authOptions.SecretKey)); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = authOptions.ValidateIssuer, ValidateAudience = authOptions.ValidateAudience, ValidateLifetime = authOptions.ValidateLifetime, ValidateIssuerSigningKey = authOptions.ValidateIssuerSigningKey, ValidIssuer = authOptions.Issuer, ValidAudience = authOptions.Audience, IssuerSigningKey = key }; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); //условная база данных с учетными записями пользователей List<User> UserDatabase = new() { new User() { Login = "user", Password ="12345" }, new User() { Login = "admin", Password = "root" }, }; app.MapGet("/", () => "Hello World!"); app.MapGet("/auth", [Authorize] () => { return Results.Ok(authOptions); }); app.MapGet("/login", (string? login, string? password) => { if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password)) return Results.BadRequest(new { Error = "Не задан логин или пароль" }); var user = UserDatabase.FirstOrDefault(u => (u.Login == login) && (u.Password == password)); if (user == null) return Results.BadRequest(new { Error = "Не верно введен логин или пароль" }); JwtSecurityToken token = new( issuer: authOptions?.Issuer, audience: authOptions?.Audience, expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(authOptions.TokenLifetime)), signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256) ); JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); string tokenString = handler.WriteToken(token); return Results.Ok(new { token = tokenString, user = login }); }); app.Run(); } } }
Итого
Сегодня мы рассмотрели процесс аутентификации пользователя с использованием JWT. Для реализации проекта нам потребовалось установить nuget-пакет Microsoft.