Содержание
До сих пор мы разрабатывали компоненты middleware, которые, если можно так выразиться, работали только слева направо — то есть, запрос либо проходил всю цепочку middleware в конвейере, либо прерывался в каком-либо middleware и пользователю возвращался код ошибки. Вместе с тем, если вспомнить схему из самой первой темы про middleware, то на ней показано прохождение запроса как слева направо, так и наоборот — справа на лево. При этом, при движении в обратном направлении мы можем изменять ответ сервера. В этой части рассмотрим вопросы связанные с модификацией ответа сервера.
Свойства класса HttpResponse
Контекст запроса, представленный в ASP.NET Core объектом типа HttpContext
, помимо прочих полезных свойств, содержит также свойство Response
типа HttpResponse
, представляющее собой данные ответа сервера. Класс HttpResponse
содержит следующие свойства:
Body |
Возвращает или задает тело ответа в виде объекта класса Stream . |
Body |
Возвращает объект типа PipeWriter для записи тела ответа |
Content |
Возвращает или задает значение заголовка Content-Length ответа. |
Content |
Возвращает или задает значение заголовка Content-Type ответа. |
Cookies |
Возвращает объект , который можно использовать для управления файлами cookie для этого ответа. |
Has |
Возвращает true , если отправка ответа уже началась |
Headers |
Возвращает заголовки ответа. |
Http |
Возвращает объект HttpContext , связанный с этим объектом HttpResponse . |
Status |
Возвращает или задает код ответа 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 раскрыта более подробно.