Привет! Представляю вашему вниманию перевод статьи "Creating a simple API Gateway in ASP.NET Core".
Время чтения: ~10 минут
В моей предыдущей статье, JWT аутентификация для микросервисов в .NET, я рассмотрел процесс создания микросервиса для аутентификации пользователей. Это может быть использовано для проверки личности пользователя перед совершением любых действий в других компонентах системы.
Другой жизненно необходимый компонент для работы продукта это API-шлюз — система между приложением и бэкэндом, которая, во-первых, маршрутизирует входящие запросы на соответствующий микросервис, и во-вторых, авторизует пользователя.
Существует много фреймворков которые могут быть использованы для создания API-шлюза, например, Ocelot в .NET core или Netflix Zuul в Java. Тем не менее, в этой статье я опишу процесс создания простого API-шлюза с нуля в .NET Core.
Создание проекта
Для начала создадим новое приложение, выбрав ASP.NET Core Web Application в окне создания проекта и Empty в качестве шаблона.
В проекте будут лежать классы Startup и Program. Для нас самой важной частью является метод Configure класса Startup. Здесь мы можем обработать входящий HTTP-запрос и ответить на него. Возможно, в методе Configure будет находится следующий код:
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello, World!");
});
Написание маршрутизатора
Так как именно в методе Configure мы будем обрабатывать запросы, напишем необходимую логику:
Router router = new Router("routes.json");
app.Run(async (context) =>
{
var content = await router.RouteRequest(context.Request);
await context.Response.WriteAsync(await content.Content.ReadAsStringAsync());
});
Сначала мы создаем объект типа Router. В его задачи входит хранение существующих маршрутов, валидация и отправка запросов согласно маршрутам. Чтобы сделать код более чистым, будем подгружать маршруты из JSON-файла.
В итоге получается следующая логика: после того, как запрос поступит на шлюз, он будет перенаправлен маршрутизатору, который, в свою очередь, отправит его на соответствующий микросервис.
Перед тем как писать класс Router, создадим файл routes.json. В этом файле укажем список маршрутов, каждый из которых будет содержать внешний адрес (endpoint) и адрес назначения (destination). Также, мы добавим флаг, сигнализирующий о необходимости авторизации пользователя перед перенаправлением.
Вот как примерно может выглядеть такой файл:
{
"routes": [
{
"endpoint": "/movies",
"destination": {
"uri": "http://localhost:8090/movies/",
"requiresAuthentication": "true"
}
},
{
"endpoint": "/songs",
"destination": {
"uri": "http://localhost:8091/songs/",
"requiresAuthentication": "false"
}
}
],
"authenticationService": {
"uri": "http://localhost:8080/api/auth/"
}
}
Создаем класс Destination
Мы теперь знаем, что каждый Route должен иметь endpoint
и destination
, а каждый Destination должен иметь поля uri
и requiresAuthentication
.
Теперь напишем класс Destination, помня о том. Я добавлю два поля, два конструктора и приватный конструктор без параметров для JSON-десериализации.
public class Destination
{
public string Uri { get; set; }
public bool RequiresAuthentication { get; set; }
public Destination(string uri, bool requiresAuthentication)
{
Uri = path;
RequiresAuthentication = requiresAuthentication;
}
public Destination(string uri)
:this(uri, false)
{
}
private Destination()
{
Uri = "/";
RequiresAuthentication = false;
}
}
Также, будет правильно написать в этом классе метод SendRequest
. Этим мы покажем, что каждый объект класса Destination будет ответственнен за отправку запроса. Этот метод будет принимать объект типа HttpRequest
, который описывает входящий запрос, вынимать оттуда всю необходимую информацию и отправлять запрос на целевой URI. Для этого напишем вспомогательный метод CreateDestinationUri
, который будет соединять строки с адресом и параметры адресной строки (query string) от клиента.
private string CreateDestinationUri(HttpRequest request)
{
string requestPath = request.Path.ToString();
string queryString = request.QueryString.ToString();
string endpoint = "";
string[] endpointSplit = requestPath.Substring(1).Split('/');
if (endpointSplit.Length > 1)
endpoint = endpointSplit[1];
return Uri + endpoint + queryString;
}
Теперь мы можем написать метод SendRequest
, который будет отправлять запрос на микросервис и получать ответ обратно.
public async Task<HttpResponseMessage> SendRequest(HttpRequest request)
{
string requestContent;
using (Stream receiveStream = request.Body)
{
using (StreamReader readStream = new StreamReader(receiveStream, Encoding.UTF8))
{
requestContent = readStream.ReadToEnd();
}
}
HttpClient client = new HttpClient();
HttpRequestMessage newRequest = new HttpRequestMessage(new HttpMethod(request.Method), CreateDestinationUri(request));
HttpResponseMessage response = await client.SendAsync(newRequest);
return response;
}
Создаем JSON-парсер.
Перед написанием класса Router, нам нужно создать логику для десериализации JSON-файла с маршрутами. Я создам для этого вспомогательный класс, в котором будет два метода: один для создания объекта из JSON-файла, а другой для десериализации.
public class JsonLoader
{
public static T LoadFromFile<T>(string filePath)
{
using (StreamReader reader = new StreamReader(filePath))
{
string json = reader.ReadToEnd();
T result = JsonConvert.DeserializeObject<T>(json);
return result;
}
}
public static T Deserialize<T>(object jsonObject)
{
return JsonConvert.DeserializeObject<T>(Convert.ToString(jsonObject));
}
}
Класс Router.
Последнее, что мы сделаем перед написанием Router — опишем модель маршрута:
public class Route
{
public string Endpoint { get; set; }
public Destination Destination { get; set; }
}
Теперь напишем класс Router, добавив туда поля и констурктор.
public class Router {
public List<Route> Routes { get; set; }
public Destination AuthenticationService { get; set; }
public Router(string routeConfigFilePath)
{
dynamic router = JsonLoader.LoadFromFile<dynamic>(routeConfigFilePath);
Routes = JsonLoader.Deserialize<List<Route>>(
Convert.ToString(router.routes)
);
AuthenticationService = JsonLoader.Deserialize<Destination>(
Convert.ToString(router.authenticationService)
);
}
}
Я использую динамический тип (dynamic type) для чтения из JSON и записи в него свойств объекта.
Теперь все готово для описания главной фнукциональности API-шлюза: маршрутизация и авторизация пользователя, которая будет происходить в методе RouteRequest
. Нам нужно распаковать базовую часть внешнего адреса (base endpoint) из объекта запроса. Например, для адреса /movies/add
базой будет /movies/
. После этого, нам нужно проверить, есть ли описание данного маршрута. Если да, то авторизуем пользователя и отправляем запрос, иначе возвращаем ошибку. Я также создал класс ConstructErrorMessage для удобства.
Для авторизации, я предпочел следующий путь: извлекаем токен из заголовка запроса и отправляем как параметр запроса. Возможен и другой вариант: оставить токен в заголовке, тогда извлекать его должен уже микросервис, которому предназначается запрос.
public async Task<HttpResponseMessage> RouteRequest(HttpRequest request)
{
string path = request.Path.ToString();
string basePath = '/' + path.Split('/')[1];
Destination destination;
try
{
destination = Routes.First(r => r.Endpoint.Equals(basePath)).Destination;
}
catch
{
return ConstructErrorMessage("The path could not be found.");
}
if (destination.RequiresAuthentication)
{
string token = request.Headers["token"];
request.Query.Append(new KeyValuePair<string, StringValues>("token", new StringValues(token)));
HttpResponseMessage authResponse = await AuthenticationService.SendRequest(request);
if (!authResponse.IsSuccessStatusCode) return ConstructErrorMessage("Authentication failed.");
}
return await destination.SendRequest(request);
}
private HttpResponseMessage ConstructErrorMessage(string error)
{
HttpResponseMessage errorMessage = new HttpResponseMessage
{
StatusCode = HttpStatusCode.NotFound,
Content = new StringContent(error)
};
return errorMessage;
}
Заключение
Создание базового API-шлюза не требует много усилий, но он не предоставит должной фукнциональности. Если вам нужен балансировщик нагрузки, вы можете посмотреть на уже существующие фреймворки или платформы, которые предлагают библиотеки для маршрутизации запросов.
Весь код из этой статьи доступен в репозитории на GitHub
Автор: acerikfy