DevGang
Авторизоваться

Использование Laravel в качестве сервисного прокси/шлюза

Мы изучали варианты реализации конечной точки прокси для одной из наших внешних служб. Одним из стеков, используемых в наших приложениях, является Laravel, поэтому, естественно, мы исследовали, как реализовать конечную точку прокси с его помощью. Оказывается, все довольно просто: Laravel уже включил большую часть материалов, необходимых для его создания. Этот подход должен подходить для многих простых вариантов использования, чтобы избежать накладных расходов на добавление нового выделенного прокси-сервиса, такого как полноценный API-шлюз, такой как Traefik или Kong, или если вам нужна пользовательская логика, которую может быть трудно достичь с помощью готовых решений.

Использование

При создании приложения с использованием микросервисов/сервис-ориентированной архитектуры обычно требуется подключение к внешним службам. Эти сервисы могут быть API какого-либо поставщика, такого как поставщик электронной почты, платежный шлюз или просто внутренняя служба, которая так или иначе оказывается в разных сетях. Некоторые распространенные случаи использования:

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

Использование laravel

С другой стороны, Laravel может быть странным выбором для создания собственного прокси-сервиса/шлюза API. Если вы намерены сделать службу отдельной службой, может иметь смысл использовать микрофреймворк, например Lumen, или просто использовать Symphony. Другие варианты — использовать другой более производительный стек, такой как Golang или NodeJs. Однако в нашем случае система на самом деле будет встроена в существующий сервис Laravel (по некоторым причинам), и поэтому мы хотим поделиться своим опытом на случай, если кто-то столкнется с похожей ситуацией, как мы.

Плюсы по сравнению с выделенным прокси-сервисом/API-шлюзом

  1. Простая в реализации пользовательская логика. При использовании выделенного сервиса вам может потребоваться использовать какой-нибудь непонятный DSL или подключить ваш скрипт каким-нибудь странным способом. Очевидно, что это может быть субъективно, но IMHO использование существующего стека является явным преимуществом, поскольку оно снижает когнитивную нагрузку на создание и обслуживание сервиса.
  2. Никаких дополнительных затрат на техническое обслуживание: не требуется осваивать новую технологию

Минусы

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

Реализация

Предпосылки

В этом руководстве мы будем использовать библиотеку 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% большинства вариантов использования. Однако, как указано в начале этой статьи, вы можете расширить этот код, включив в него больше функциональных возможностей. Например, вы могли бы добавить сюда механизм аутентификации или некоторое кэширование, чтобы уменьшить количество сетевых запросов.

#Laravel
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу