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


