Аутентификация и авторизация в ASP.NET Core. Аутентификация на основе JWT-токенов

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

Аутентификация на основе JWT-токенов

Чтобы аутентифицировать пользователя с использованием JWT-токена мы должны каким-либо образом провести проверку пользователя (например, запросить логин/пароль), затем сформировать для пользователя JWT-токен, отправить пользователю токен.

Создадим новый пустой проект ASP.NET Core, добавим в него nuget-пакет Microsoft.AspNetCore.Authentication.JwtBearer и сразу подключим в проект следующие пространства имен, которые в дальнейшем будем использовать:

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": "*"
}
Внимание! Секретный ключ для подписи токена сохранен в файле appsettings.json исключительно для демонстрации работы с JWT-токенами. Никогда не храните конфиденциальные данные в открытом виде

После получения настроек из конфигурации мы создаем ключ для подписи токена:

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.AspNetCore.Authentication.JwtBearer . Для настройки параметров аутентификации и проверки токена мы использовали лишь часть доступных нам настроек. После получения токена пользователь должен отсылать его на сервер в заголовке Authorization. Для проверки работы токена мы (пока) использовали сервис ReqBin с помощью которого проверили доступ к ресурсу приложения, который требует авторизации.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии