ASP.NET Core Web API. Способы создания компонентов middleware

В зависимости от наших потребностей, в ASP.NET Core мы можем создавать компоненты middleware различными способами. В этой части мы рассмотрим возможные способы создания компонентов middleware при разработке приложений Web API на одном примере, чтобы наглядно продемонстрировать преимущества и недостатки того или иного способа.

Введение

Немаловажным элементом практически любого Web API является система аутентификации и авторизации пользователей. В ASP.NET Core уже имеются достаточно удобные и функциональные возможности использования аутентификации и авторизации пользователей, которые также будут рассмотрены. При этом, не всегда нам может потребоваться какая-то сложная система безопасности с распределением пользователей по ролям, использованием политик безопасности и так далее. Иногда, например, с целью тестирования работы всего API, требуется, чтобы пользователь просто передавал в URL или в заголовках запроса какой-нибудь токен доступа и на основании значения этого токена приложение каким-либо образом выстраивало работу с запросами пользователя.

Для таким целей мы можем написать небольшой компонент middleware, который будет проверять токен доступа и либо блокировать дальнейшее выполнение запроса пользователя, либо пропускать запрос дальше в контроллер для обработки. Именно такой компонент middleware мы разработаем сегодня, используя различные способы создания middleware в ASP.NET Core.

Использование лямбда-выражений

С использованием этого способа создания компонентов middleware мы познакомились изначально. Используя лямбда-выражения, мы создаем так называемые встроенные (inline) компоненты middleware. Посмотрим, как будет выглядеть компонент middleware проверки токена, если мы используем этот способ. Для дальнейшего сравнения различных способов создания middleware будем приводить весь исходный код файла Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();

//псевдо-БД с действительными значениями токенов
string[] valid_tokens = ["12345", "abcd", "password"];

app.Use(async (context, next) => 
{
    //пробуем получить значение токена из запроса пользователя
    var token = context.Request.Query["token"];//пытаемся получить токен из параметров URI
    if (string.IsNullOrWhiteSpace(token))
    {
        token = context.Request.Headers["X-Auth"];//пытаемся получить токен из заголовка X-Auth
    }

    if (string.IsNullOrWhiteSpace(token)) //не смогли получить токен
        context.Response.StatusCode = 401; //возвращаем код 401 пользователю
    else //получили токен - необходимо проверить, что токен действителен
    {
        if (valid_tokens.Contains<string>(token))
            await next(context); //токен верный - пропускаем запрос дальше до следующего middleware
        else
            context.Response.StatusCode = 419; //419 - Authentication Timeout
    }
});

app.MapControllers();
app.Run();

Так как наш middleware должен либо пропускать запрос далее по конвейеру, либо прерывать выполнение запроса, для встраивания middleware в конвейер обработки запроса мы используем метод Use(). На первом шаге мы пытаемся получить значение токена либо из строки параметров запроса, либо из заголовка — для этого мы используем свойства объекта HttpRequest:

//пробуем получить значение токена из запроса пользователя
var token = context.Request.Query["token"];//пытаемся получить токен из параметров URI
if (string.IsNullOrWhiteSpace(token))
{
    token = context.Request.Headers["X-Auth"];//пытаемся получить токен из заголовка X-Auth
}

Далее, если токен не был получен вообще, то пользователю вернется код статуса 401 Unauthorized и выполнение запроса прервется. Если же токен получен, то его необходимо проверить. Для этой цели в нашем приложении создается псевдо-БД с верными значениями токенов:

//псевдо-БД с действительными значениями токенов
string[] valid_tokens = ["12345", "abcd", "password"];

В зависимости от результата проверки мы либо возвращаем пользователю код ошибки 419, либо пропускаем запрос далее по конвейеру:

if (valid_tokens.Contains<string>(token))
    await next(context); //токен верный - пропускаем запрос дальше до следующего middleware
else
    context.Response.StatusCode = 419; //419 - Authentication Timeout

Сразу проверим работу приложения. Проверку этого примера необходимо осуществлять без использования интерфейса Swagger, то есть записывать URI доступа непосредственно в строке браузера:

если токен не определен

если токен не верный

если токен верный

Чтобы проверить передачу токена через заголовок, можно воспользоваться любым сервисом тестирования Web API, например, ReqBin:

Итак, наш пример работает. Можно было бы продумать вариант, когда два разных токена передаются одновременно и в заголовке и в строке запроса, но не будем сильно перегружать наш пример проверками. Что можно отметить при использовании лямбда-выражений при создании компонентов middleware? Здесь код компонента располагается непосредственно внутри метода Use(). Такой подход может быть удобным, если middleware выполняет минимальное количество операций, расположенных на нескольких строках. Если же предполагается, что middleware будет содержать более-менее сложный код, то использование лямбда-выражения может усложнить чтение кода приложения. Уже сейчас, несмотря на то, что код нашего middleware не такой уж и большой, становится не совсем комфортно оценивать целиком состав конвейера обработки запросов. Чтобы немного «разгрузить» основной код мы можем воспользоваться следующим способом создания middleware.

Компонент middleware как отдельный метод

При необходимости, мы можем определить компонент middleware как отдельный метод класса. В нашем случае, мы могли бы создать следующий метод:

async Task CheckToken(HttpContext context, RequestDelegate next)
{
    //пробуем получить значение токена из запроса пользователя
    var token = context.Request.Query["token"];//пытаемся получить токен из параметров URI
    if (string.IsNullOrWhiteSpace(token))
    {
        token = context.Request.Headers["X-Auth"];//пытаемся получить токен из заголовка X-Auth
    }

    if (string.IsNullOrWhiteSpace(token)) //не смогли получить токен
        context.Response.StatusCode = 401; //возвращаем код 401 пользователю
    else //получили токен - необходимо проверить, что токен действителен
    {
        if (valid_tokens.Contains<string>(token))
            await next(context); //токен верный - пропускаем запрос дальше до следующего middleware
        else
            context.Response.StatusCode = 419; //419 - Authentication Timeout
    }
}

и использовать этот метод следующим образом:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

//псевдо-БД с действительными значениями токенов
string[] valid_tokens = ["12345", "abcd", "password"];

app.Use(CheckToken); //используем middleware

app.MapControllers();

app.Run();

Как можно видеть, код приложения становится короче и читать такой код по-удобнее. Однако, если вернуться немного назад и посмотреть на то, как используются различные middleware, то чаще всего для встраивания middleware в конвейер используются методы расширения типа UseHttpsRedirection() или UseAuthorization(). Если мы решам использовать методы расширения для добавления своих компонентов middleware в конвейер, то здесь нам будет необходимо воспользоваться следующими способом создания компонентов middleware.

Фабричные классы middleware

Начиная с ASP.NET Core 2.0 мы можем реализовать интерфейс IMiddleware, который определяет ПО промежуточного слоя и использовать фабрику IMiddlewareFactory для активации полученного компонента middleware. Воспользуемся этой возможностью и создадим компонент middleware, как отдельный класс. Добавим в проект новый класс с названием FactoryActivatedSimpleAuth. Этот класс должен реализовывать интерфейс IMiddleware. У этого интерфейса определен всего один метод:

Task InvokeAsync(HttpContext context, RequestDelegate next);

Код класса представлен ниже:

namespace WebApiAuth
{
    public class FactoryActivatedSimpleAuth: IMiddleware
    {
        private readonly string[] valid_tokens = ["12345", "abcd", "password"];

        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            //пробуем получить значение токена из запроса пользователя
            var token = context.Request.Query["token"];//пытаемся получить токен из параметров URI
            if (string.IsNullOrWhiteSpace(token))
            {
                token = context.Request.Headers["X-Auth"];//пытаемся получить токен из заголовка X-Auth
            }

            if (string.IsNullOrWhiteSpace(token)) //не смогли получить токен
                context.Response.StatusCode = 401; //возвращаем код 401 пользователю
            else //получили токен - необходимо проверить, что токен действителен
            {
                if (valid_tokens.Contains<string>(token))
                    await next(context); //токен верный - пропускаем запрос дальше до следующего middleware
                else
                    context.Response.StatusCode = 419; //419 - Authentication Timeout
            }
        }
    }
}

Теперь нам необходимо использовать этот класс как middleware в конвейере обработки запросов. Для этого мы должны выполнить два действия.

1. Зарегистрировать класс middleware как сервис. Делается это следующим образом:

builder.Services.AddTransient<FactoryActivatedSimpleAuth>();

О сервисах в ASP.NET Core и их регистрации более подробная информация будет позднее. Пока же отметим, что вызов этого метода должен находиться ДО строки:

var app = builder.Build();

Для того, чтобы добавить компонент middleware в конвейер обработки запросов, нам необходимо воспользоваться методом IApplicationBuilder UseMiddleware<T> следующим образом:

app.UseMiddleware<FactoryActivatedSimpleAuth>();

В итоге, весь код файла Program.cs становится таким:

using WebApiAuth;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddTransient<FactoryActivatedSimpleAuth>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();

app.UseMiddleware<FactoryActivatedSimpleAuth>();

app.MapControllers();

app.Run();

Как видите, код стало проще читать — мы сразу видим то, какие middleware подключаются в конвейер, а вызов UseMiddleware<T>() говорит нам о том, что активируется свой middleware, код которого можно найти в проекте. Здесь стоит отметить, что перечень доступных токенов жестко закодирован внутри класса middleware. Этот недостаток можно будет ликвидировать позднее, когда будут изучены основы использования внедрения зависимостей в ASP.NET Core. В остальном же, мы получаем в качестве преимущества, как минимум, строгую типизацию. При необходимости, мы можем предусмотреть в приложении поиск всех классов, реализующих IMiddleware для их регистрации и добавления в конвейер обработки запросов.

Компонент middleware как отдельный класс с активацией по соглашению

Компонент middleware может быть оформлен в приложении, также как и обычный класс C#, который не реализуется интерфейс IMiddleware. Такой способ создания класса использует так называемую активацию по соглашению, то есть ASP.NET Core будет воспринимать класс именно как компонент middleware, если этот класс будет использовать заранее оговоренные соглашения, а именно:

  • Содержать открытый конструктор с параметром типа RequestDelegate.
  • Содержать открытый метод с именем Invoke или InvokeAsync, который должен:
    • вернуть Task;
    • принимать первым параметром объект типа HttpContext.

Следуя этим соглашениям, создадим класс компонента middleware. Для этого добавим в проект новый файл с названием SimpleAuth.cs и добавим в этот файл класс со следующим содержимым:

namespace WebApiAuth
{
    public class SimpleAuth
    {
        readonly string[] valid_tokens = ["12345", "abcd", "password"];
        private readonly RequestDelegate _next;

        //конструктор с параметром
        public SimpleAuth(RequestDelegate next) 
        {
            _next = next;
        }

        //открытый метод 
        public async Task InvokeAsync(HttpContext context)
        {
            var token = context.Request.Query["token"];//пытаемся получить токен из параметров URI
            if (string.IsNullOrWhiteSpace(token))
            {
                token = context.Request.Headers["X-Auth"];//пытаемся получить токен из заголовка X-Auth
            }

            if (string.IsNullOrWhiteSpace(token)) //не смогли получить токен
                context.Response.StatusCode = 401; //возвращаем код 401 пользователю
            else //получили токен - необходимо проверить, что токен действителен
            {
                if (valid_tokens.Contains<string>(token))
                    await _next(context); //токен верный - пропускаем запрос дальше до следующего middleware
                else
                    context.Response.StatusCode = 419; //419 - Authentication Timeout
            }
        }
    }
}

Как видите, в этом классе мы применили необходимые соглашения — добавили открытый конструктор с параметром, а метод с именем InvokeAsync первым (и пока единственным) параметром принимает контекст запроса, а возвращает Task.  Теперь необходимо добавить middleware в конвейер обработки запросов. Для этого мы можем также использовать метод UseMiddleware<T>:

using WebApiAuth;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();

app.UseMiddleware<SimpleAuth>();
app.MapControllers();

app.Run();

В отличие от способа создания middleware на основе фабрики, при использовании такого подхода, мы можем передавать в компонент middleware дополнительные параметры, не задействуя внедрение зависимостей.

Передача параметров в middleware

Использование компонента middleware как класса с активацией по соглашению, позволяет также передавать в middleware дополнительные параметры без использования внедрения зависимостей. Например, мы можем передавать извне перечень доступных токенов и какие-то дополнительные настройки для компонента. Обычно, дополнительные параметры middleware передаются в виде классов. Создадим новый класс в котором будем хранить настройки для нашего компонента middleware:

namespace WebApiAuth
{
    public enum TokenPosition 
    { 
        QueryAndHeader, 
        OnlyQuery, 
        OnlyHeader
    }
    
    public class SimpleAuthOptions
    {
        public string[] ValidTokens { get; set; } = [];
        public TokenPosition Position { get; set; }
    }
}

здесь, для примера, класс с дополнительными параметрами содержит два свойства — массив доступных токенов и перечисление, указывающее где необходимо проверять наличие токена: в строке запроса и заголовке, только в строке запроса, только в заголовке. Теперь добавим в конструктор класса middleware новый параметр типа SimpleAuthOptions и немного перепишем код класса:

public class SimpleAuth
{
    readonly SimpleAuthOptions _options; //Настройки
    private readonly RequestDelegate _next;

    //конструктор с параметром
    public SimpleAuth(RequestDelegate next, SimpleAuthOptions options)
    {
        _next = next;
        _options = options;
    }

    //открытый метод 
    public async Task InvokeAsync(HttpContext context)
    {
        if (TryGetToken(context, out string? token))
        {
            if (_options.ValidTokens.Any(t => t == token))
                await _next(context); //токен верный - пропускаем запрос дальше до следующего middleware
            else
                context.Response.StatusCode = 419; //419 - Authentication Timeout
        }
        else
        {
            context.Response.StatusCode = 401; //возвращаем код 401 пользователю
        }

    }

    private bool TryGetToken(HttpContext context, out string? token)
    {
        switch (_options.Position)
        {
            case TokenPosition.QueryAndHeader:
                {
                    token = context.Request.Query["token"];//пытаемся получить токен из параметров URI
                    if (string.IsNullOrWhiteSpace(token))
                    {
                        token = context.Request.Headers["X-Auth"];//пытаемся получить токен из заголовка X-Auth
                    }
                    break;
                }
            case TokenPosition.OnlyQuery:
                {
                    token = context.Request.Query["token"];//пытаемся получить токен из параметров URI
                    break;
                }
            case TokenPosition.OnlyHeader:
                {
                    token = context.Request.Headers["X-Auth"];//пытаемся получить токен из заголовка X-Auth
                    break;
                }
            default:
                {
                    token = string.Empty;
                    break;
                };
        }
        return string.IsNullOrWhiteSpace(token) == false;
    }
}

Теперь поиск токена в запросе осуществляется с помощью метода TryGetToken(), а в методе InvokeAsync() токен проверяется на правильность. Чтобы компонент middleware мог принимать параметры, перепишем в файле Program.cs вызов метода UseMiddleware() следующим образом:

app.UseMiddleware<SimpleAuth>(new SimpleAuthOptions()
{
    ValidTokens = valid_tokens,
    Position = TokenPosition.QueryAndHeader
});

то есть, в параметрах метода мы просто передаем экземпляр класса SimpleAuthOptions и наш компонент middleware может, в дальнейшем, использовать дополнительные настройки в своей работе. Стоит только отметить, что все дополнительные параметры конструктора класса middleware должны следовать за параметром типа RequestDelegate, чтобы не нарушались соглашения. Таким образом мы можем передавать в middleware не один класс, а, например, несколько. если это потребуется.

Методы расширения для добавления middleware в конвейер обработки запросов

Когда мы создаем компонент middleware как отдельный класс, то мы можем создать свой метод расширения для IApplicationBuilder. Например, мы можем добавить такой метод расширения для класса SimpleAuth

public static class SimpleAuthWxtension
{
    public static IApplicationBuilder UseSimpleAuth(this IApplicationBuilder app, SimpleAuthOptions options)
    {
        return app.UseMiddleware<SimpleAuth>(options);
    }
}

и использовать этот метод расширения в приложении:

using WebApiAuth;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();

//псевдо-БД с действительными значениями токенов
string[] valid_tokens = ["12345", "abcd", "password"];

//подключаем middleware
app.UseSimpleAuth(new SimpleAuthOptions()
{
    ValidTokens = valid_tokens,
    Position = TokenPosition.QueryAndHeader
});

app.MapControllers();
app.Run();

Обычно, методы расширения для middleware начинаются со строки Use, как в нашем случае — метод имеет название UseSimpleAuth(). Использование методов расширения может немного повысить выразительность кода. Также, мы можем вынести какую-то часть кода из основной части приложения в метод расширения, если это потребуется.

Итого

Мы рассмотрели три способа создания компонентов middleware в ASP.NET Core — с использованием лямбда-выражений и отдельных методов с параметром типа RequestDelegate, с использованием т.н. активации на основе фабрики и с использованием активации по соглашению. Каждый способ имеет право на использование в приложениях ASP.NET Core в зависимости от наших потребностей. При этом, если предполагается, что middleware будет иметь достаточно большой размер, использовать какие-либо внешние данные, помимо данных запроса, то лучше использовать способ создания middleware как отдельного класса.

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