ASP.NET Core Web API. Работа с ответом в middleware

До сих пор мы разрабатывали компоненты middleware, которые, если можно так выразиться, работали только слева направо — то есть, запрос либо проходил всю цепочку middleware в конвейере, либо прерывался в каком-либо middleware и пользователю возвращался код ошибки. Вместе с тем, если вспомнить схему из самой первой темы про middleware, то на ней показано прохождение запроса как слева направо, так и наоборот — справа на лево. При этом, при движении в обратном направлении мы можем изменять ответ сервера. В этой части рассмотрим вопросы связанные с модификацией ответа сервера.

Свойства класса HttpResponse

Контекст запроса, представленный в ASP.NET Core объектом типа HttpContext, помимо прочих полезных свойств, содержит также свойство Response типа HttpResponse, представляющее собой данные ответа сервера. Класс HttpResponse содержит следующие свойства:

Body Возвращает или задает тело ответа в виде объекта класса Stream .
BodyWriter Возвращает объект типа PipeWriter для записи тела ответа
ContentLength Возвращает или задает значение заголовка Content-Length ответа.
ContentType Возвращает или задает значение заголовка Content-Type ответа.
Cookies Возвращает объект , который можно использовать для управления файлами cookie для этого ответа.
HasStarted Возвращает true, если отправка ответа уже началась
Headers Возвращает заголовки ответа.
HttpContext Возвращает объект HttpContext, связанный с этим объектом HttpResponse.
StatusCode Возвращает или задает код ответа HTTP.

Одним из этих свойств, а именно StatusCode, мы уже пользовались. Рассмотрим, как мы можем использовать другие свойства класса HttpResponse. В качестве примера, немного доработаем нашу простенькую систему аутентификации пользователей из предыдущей части, которая на данный момент состоит всего из одного компонента middleware, оформленного в виде класса:

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;
    }
}

Сейчас сервер отправляет пользователю только код состояния по которому мы можем судить об успешности или провале выполнения запроса. Доработаем этот пример так, чтобы помимо кода состояния пользователь мог также получать и текстовую информацию об ошибке, а, заодно, реализуем процесс прохождения запроса справа налево.

Построение конвейера обработки запросов

Компонент middleware для модификации ответа сервера

В качестве примера, добавим в наше приложение ещё один компонент middleware, который будет по значению свойства Response.StatusCode формировать текст ошибки. Класс будет выглядеть следующим образом:

public class AuthStatusMiddleware
{
    private readonly RequestDelegate _next;

    public AuthStatusMiddleware(RequestDelegate next)
    {
        _next = next;   
    }

    public async Task InvokeAsync(HttpContext context) 
    {
        await _next(context); //пропускаем запрос в следующий middleware

        if (context.Response.HasStarted == false)
            context.Response.ContentType = "text/plain; charset=utf-8";

        switch (context.Response.StatusCode) 
        {
            case 401: 
                {
                    await context.Response.WriteAsync("Ошибка авторизации. Не предоставлен токен доступа");
                    break; 
                }
            case 419: 
                {
                    await context.Response.WriteAsync("Ошибка авторизации. Токен доступа не действительный");
                    break; 
                }
                default: 
                {
                    break;
                }
        }
    }
}

Здесь стоит отметить два момента. Первый — вызов следующего middleware расположен в самом начале метода:

await _next(context); //пропускаем запрос в следующий middleware

то есть, при движении запроса от слева направо (от первого к последнему middleware в конвейере) наш компонент middleware не выполняет никакой работы, а сразу пропускает запрос далее по конвейеру. Когда запрос будет проходить конвейер в обратном направлении — сработает весь код, расположенный в методе под строкой await _next(context).

Второй момент — запись заголовка Content-Type:

if (context.Response.HasStarted == false)
    context.Response.ContentType = "text/plain; charset=utf-8";

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

  • Заголовки отправляются вместе с этим фрагментом текста клиенту.
  • Изменить заголовки ответов больше невозможно.

Если не производить такую проверку, то, в случае, если запрос будет успешно выполнен (токен будет верный и контроллер сформирует массив данных), то на этапе прохождения запроса через наш middleware уже будет сформирован ответ и попытка изменить заголовок приведет к ошибке. Если же не включать заголовок Content-Type в ответ при получении кода статуса отличного от 200, то в браузере мы увидим не русский текст, а «кракозябры».

Построение конвейера обработки запросов

Теперь необходимо в правильном порядке выстроить конвейер обработки запросов. В Program.cs добавляем два компонента middleware — SimpleAuth и новый AuthStatusMiddleware. Причем, новый компонент должен идти перед SimpleAuth:

using WebApiAuth;

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

var app = builder.Build();

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

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

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

//добавляем компоненты в конвейер
app.UseMiddleware<AuthStatusMiddleware>();
app.UseMiddleware<SimpleAuth>(new SimpleAuthOptions()
{
    ValidTokens = valid_tokens,
    Position = TokenPosition.QueryAndHeader
});

app.MapControllers();

app.Run();

Запустим приложение и убедимся, что наше приложение работает так, как и предполагалось. Результат работы с ошибочным токеном:

Верный токен:

Для удобства, мы можем создать свой метод расширения для IApplicationBuilder, который будет добавлять оба middleware в необходимом нам порядке в конвейер обработки запроса:

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

и заменить методы UseMiddleware<T>() на один вызов:

app.UseSimpleAuth(new SimpleAuthOptions()
{
    ValidTokens = valid_tokens,
    Position = TokenPosition.QueryAndHeader
});

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

Итого

На данный момент мы разобрали основные моменты использования компонентов middleware в приложениях ASP.NET Core Web API. Полученной информации должно хватить для того, чтобы перейти к следующей теме. При этом, «за кадром» остались такие моменты, как ветвление конвейера обработки запросов. Так как мы рассматриваем вопросы, связанные в первую очередь в разработкой Web API на основе контроллеров, то вопросы связанные с ветвлением конвейера не столь важны, как, например, темы маршрутизации с использованием атрибутов или внедрение зависимостей. Однако, если вам необходимо более подробно разобраться с этим и другими вопросами, связанными с middleware в ASP.NET Core, то можно обратиться к руководству по «базовому» ASP.NET Core в котором тема middleware раскрыта более подробно.

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