Использование Laravel в качестве сервисного прокси/шлюза
Мы изучали варианты реализации конечной точки прокси для одной из наших внешних служб. Одним из стеков, используемых в наших приложениях, является Laravel, поэтому, естественно, мы исследовали, как реализовать конечную точку прокси с его помощью. Оказывается, все довольно просто: Laravel уже включил большую часть материалов, необходимых для его создания. Этот подход должен подходить для многих простых вариантов использования, чтобы избежать накладных расходов на добавление нового выделенного прокси-сервиса, такого как полноценный API-шлюз, такой как Traefik или Kong, или если вам нужна пользовательская логика, которую может быть трудно достичь с помощью готовых решений.
Использование
При создании приложения с использованием микросервисов/сервис-ориентированной архитектуры обычно требуется подключение к внешним службам. Эти сервисы могут быть API какого-либо поставщика, такого как поставщик электронной почты, платежный шлюз или просто внутренняя служба, которая так или иначе оказывается в разных сетях. Некоторые распространенные случаи использования:
- ограничить доступ к учетным данным. С помощью одной прокси-службы другим службам не нужно было хранить учетные данные для внешних служб, что обеспечивает лучший контроль с точки зрения обслуживания и безопасности.
- общий ресурс, например: токен oauth. Каждой службе не нужно было запрашивать свой собственный токен, но вместо этого она могла совместно использовать тот же токен, который обрабатывался прокси-службой. Другим примером является кэширование ресурса, поэтому к часто используемым ресурсам необходимо обращаться всего несколько раз.
Использование laravel
С другой стороны, Laravel может быть странным выбором для создания собственного прокси-сервиса/шлюза API. Если вы намерены сделать службу отдельной службой, может иметь смысл использовать микрофреймворк, например Lumen, или просто использовать Symphony. Другие варианты — использовать другой более производительный стек, такой как Golang или NodeJs. Однако в нашем случае система на самом деле будет встроена в существующий сервис Laravel (по некоторым причинам), и поэтому мы хотим поделиться своим опытом на случай, если кто-то столкнется с похожей ситуацией, как мы.
Плюсы по сравнению с выделенным прокси-сервисом/API-шлюзом
- Простая в реализации пользовательская логика. При использовании выделенного сервиса вам может потребоваться использовать какой-нибудь непонятный DSL или подключить ваш скрипт каким-нибудь странным способом. Очевидно, что это может быть субъективно, но IMHO использование существующего стека является явным преимуществом, поскольку оно снижает когнитивную нагрузку на создание и обслуживание сервиса.
- Никаких дополнительных затрат на техническое обслуживание: не требуется осваивать новую технологию
Минусы
- Дополнительные рабочие переходы по сравнению с прямым доступом, хотя это зависит от ситуации. Кэширование может фактически улучшить сетевое подключение
- Ограниченная масштабируемость
Реализация
Предпосылки
В этом руководстве мы будем использовать библиотеку Guzzle в качестве HTTP-клиента. некоторые из вас могут задаться вопросом: «Почему бы нам не использовать встроенный HTTP-клиент Laravel?». Что ж, клиент Laravel проще в использовании, но он менее гибкий для наших нужд. HTTP-клиент Laravel на самом деле является просто оболочкой Guzzle, поэтому вполне логично, что он может быть менее гибким.
Чтобы установить guzzle, просто запустите
composer require guzzlehttp/guzzle
Также для краткости, здесь мы бы реализовали конечную точку прямо в нашем файле маршрута. На практике было бы чище разделить логику в выделенный файл контроллера. Здесь вы могли бы выбрать либо routes/web.php
или routes/api.php
, или создать новый файл маршрута, если вы захотите.
Основное использование
Давайте начнем с простого. Здесь мы создадим новую конечную точку, которая будет вызывать httpbin.org с учетом пути и метода HTTP.
use GuzzleHttp\Client as HttpClient;
Route::any('/proxy/{path}', function(Request $req, $path) {
$client = new HttpClient([
'base_uri' => 'https://httpbin.org'
]);
return $client->request($req->method(), $path);
});
Код должен быть довольно простым. для каждого запроса к /proxy/{path}
, он будет отправлять запрос на httpbin.org/{path}
с помощью того же HTTP-метода. Вы могли бы попробовать запросить новую конечную точку, используя различные методы, чтобы увидеть ее в действии. Здесь мы использовали HTTPie для тестирования конечной точки:
http POST localhost:8000/proxy/post
http PUT localhost:8000/proxy/put
http DELETE localhost:8000/proxy/delete
Прокси-подпути
Если вы заметили, переменная path на самом деле способна извлекать только путь, но не подпуть. Например, получение localhost:8000/proxy/get
работает, но localhost:8000/proxy/get/subpath
завершится неудачей, поскольку laravel не сможет маршрутизировать более поздний. Решение состоит в том, чтобы просто добавить метод 'where
", чтобы позволить переменной path
перехватывать все подпути. так что просто добавьте:
Route::any(
//...
)->where('path', '.*');
Добавление тела запроса, параметров и кода ответа
Вы также можете заметить, что наша текущая реализация не пересылает тело запроса, параметры запроса и код состояния ответа. Поэтому давайте изменим это:
//...
$resp = $client->request($req->method(), $path, [
'query' => $req->query(),
'body' => $req->getContent(),
]);
return response($resp->getBody()->getContents(), $resp->getStatusCode());
//...
Пересылка необходимых заголовков
Наконец, вы также можете понять, что наша реализация не пересылает заголовки как запроса, так и ответа. Заголовки на самом деле довольно сложны, поскольку они могут повлиять на наш запрос и ответ и сделать их недействительными. Мы обнаружили, что лучше всего пересылать только необходимые поля заголовка и игнорировать остальные. Это также повышает нашу безопасность.
Чтобы сделать это, мы подготовим вспомогательную функцию для фильтрации заголовков и извлечения только заголовков 'content-type' и 'accept'. Конечно, вы также можете изменить его в соответствии с вашими потребностями:
// simple helper function to filter header array on request & response
function filterHeaders($headers) {
$allowedHeaders = ['accept', 'content-type'];
return array_filter($headers, function($key) use ($allowedHeaders) {
return in_array(strtolower($key), $allowedHeaders);
}, ARRAY_FILTER_USE_KEY);
}
и тогда мы могли бы использовать его в наших конечных точках. Наш финал мог бы быть таким:
<?php
// you could use either routes/web.php or routes/api.php
// simple helper function to filter header array on request & response
function filterHeaders($headers) {
$allowedHeaders = ['accept', 'content-type'];
return array_filter($headers, function($key) use ($allowedHeaders) {
return in_array(strtolower($key), $allowedHeaders);
}, ARRAY_FILTER_USE_KEY);
}
Route::any('/proxy_example/{path}', function(Request $request, $path) {
$client = new GuzzleHttp\Client([
// Base URI is used with relative requests
'base_uri' => 'https://pie.dev', // public dummy API for example
// You can set any number of default request options.
'timeout' => 60.0,
'http_errors' => false, // disable guzzle exception on 4xx or 5xx response code
]);
// create request according to our needs. we could add
// custom logic such as auth flow, caching mechanism, etc
$resp = $client->request($request->method(), $path, [
'headers' => filterHeaders($request->header()),
'query' => $request->query(),
'body' => $request->getContent(),
]);
// recreate response object to be passed to actual caller
// according to our needs.
return response($resp->getBody()->getContents(), $resp->getStatusCode())
->withHeaders(filterHeaders($resp->getHeaders()));
})->where('path', '.*'); // required to allow $path to catch all sub-path
Итог
Эта реализация должна охватывать 80-90% большинства вариантов использования. Однако, как указано в начале этой статьи, вы можете расширить этот код, включив в него больше функциональных возможностей. Например, вы могли бы добавить сюда механизм аутентификации или некоторое кэширование, чтобы уменьшить количество сетевых запросов.