Часто API защищены уникальным APIKEY для каждого приложения. Требование этих API заключается в том, что вы отправляете свой APIKEY с каждым запросом либо через строку запроса, либо в пользовательском заголовке. Но когда ваше приложение является одностраничным приложением (SPA), вы не можете встроить свой APIKEY в свое приложение, потому что это небезопасный способ обработки конфиденциальных данных, а поддержка CORS не включена.

Чтобы решить эту проблему обработки защищенных данных при написании потребителя SPA, вы должны использовать прокси-приложение. Прокси-сервер примет все запросы от SPA, защитит их с помощью CORS, добавит APIKEY и перенаправит запрос в защищенный API. И он перенаправит ответ от API обратно в запрашивающее SPA.

Мы будем делать вызов API к Yahoo! Финансовый API на RapidAPI. Этот API использует APIKEY и является хорошим примером для этого руководства. Используемый здесь метод может быть применен к любому API, для которого требуется APIKEY.

В рамках Спецификации HTTP API могут выполнять запросы, в том числе с использованием следующих HTTP-методов:

Прокси-приложение может обрабатывать любой или все из этих пяти HTTP-команд. В этой статье описывается, как создать прокси-приложение GET для использования API.

Прокси-контроллер принимает один параметр с кодировкой urlencoded, url, в одной конечной точке RPC в следующем формате:

https://proxy.server/proxy?url=%2Fmarket%2Fget-summary%2F1

Затем ваш ProxyAPI RPC может обработать URL-адрес и добавить APIKEY к запросу, а затем перенаправить его в API данных. Это позволяет вам писать запросы, как если бы они были непосредственно в API.

Пример прокси-кода

Это действие контроллера, написанное на PHP в Zend Framework, реализует прокси для API для запросов GET. Пожалуйста, смотрите комментарии по всему коду.

use Exception;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\Uri\UriFactory;
use Zend\Http\Client;
use Zend\Http\Request;
use Zend\Http\Response;
use ZF\ApiProblem\ApiProblem;
use ZF\ApiProblem\ApiProblemResponse;

class ProxyController extends AbstractActionController
{
    private $config = [
        'x-rapidapi-host' => 'apidojo-yahoo-finance-v1.p.rapidapi.com',
        'x-rapidapi-key' => "12345678901234567890123456789012345678901234567890",
        'region' => 'US',
        'language' => 'en',
    ];

    public function proxyAction()
    {
        $url = $this->params()->fromQuery('url');

        // Extract the URI, append region and language to request
        $uri = UriFactory::factory('https://' . $this->config['x-rapidapi-host'] . '/' . $url);
        $query = $uri->getQueryAsArray();
        $query['region'] = $this->config['region'];
        $query['lang'] = $this->config['language'];
        $uri->setQuery($query);

        // Run proxy based on the request method which with this was called.
        switch ($this->getRequest()->getMethod()) {
            case 'GET':
                // Here would be a good spot to add caching

                // Handle GET query requests with no body
                $client = new Client((string) $uri);
                $client->setMethod($this->getRequest()->getMethod());

                // Copy all headers from request to this server into the
                // request for the API call.  Append x-rapidapi* headers
                $client->getRequest()
                    ->getHeaders()
                        ->addHeaders($this->getRequest()->getHeaders())
                        ->addHeaderLine('Accept', 'application/json')
                        ->addHeaderLine('x-rapidapi-key', $this->config['x-rapidapi-key'])
                        ->addHeaderLine('x-rapidapi-host', $this->config['x-rapidapi-host'])
                ;

                try {
                    // Try making the API call
                    $response = $client->send();
                    if ($response->getStatusCode() !== 200) {
                        // Use API Problem to return a non-200 status code which did not
                        // trigger an exception.
                        $apiProblem = new ApiProblem(
                            $response->getStatusCode(),
                            $response->getBody()
                        );
                        $apiProblemResponse = new ApiProblemResponse($apiProblem);

                        return $apiProblemResponse;
                    }
                } catch (Exception $e) {
                    // Handle all exceptions with ApiProblem
                    $apiProblem = new ApiProblem(500, $e->getMessage() . ' ' . (string) $uri);
                    $apiProblemResponse = new ApiProblemResponse($apiProblem);

                    return $apiProblemResponse;
                }

                // Return the response from the api directly to the requsting 
                // API consumer
                return $response;

            case 'POST':
            case 'PATCH':
            case 'PUT':
            case 'DELETE':
            default:
                return $this->getResponse();
        }
    }
}

| CORS — это метод, позволяющий совместно использовать ресурсы между сценариями, работающими в клиенте браузера, и ресурсами из другого источника.

CORS используется для обмена данными между браузером и API. CORS не используется при обмене данными между сервером и API. Поскольку SPA полностью запускаются в браузере, для доступа к API-интерфейсу прокси-сервера используются соответствующие заголовки CORS. Существует множество библиотек для реализации CORS. Я использую модуль https://github.com/zf-fr/zfr-cors. Вам нужно будет внедрить CORS, иначе вы увидите ошибку, аналогичную Нет заголовка Access-Control-Allow-Origin в запрошенном ресурсе.

Звонки на прокси из SPA

Вот фрагмент TypeScript, который использует прокси для вызова API к Yahoo! API на RapidAPI:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '@env';
/**
 * For brevity the MarketSymbol class does not include all fields from
 * an API response.
 */
class MarketSymbol {
  symbol: string;
  fullExchangeName: string;
  market: string;
  regularMarketPrice: {
    raw: number;
    fmt: number;
  };
}
/**
 * This is the structure of the response expected from the
 * MarketSummary API call.
 */
class MarketSummary {
  marketSummaryResponse: {
    result: Array<MarketSymbol>;
    error: any;
  };
}
@Injectable({
  providedIn: 'root'
})
export class YahooFinanceProxyService {
  /**
   *  The configuration looks like
   */
  //  export const environment = {
  //   production: false,
  //   apiUrl: 'https://proxy.server/proxy?url='
  //  }
  private apiUrl = environment.apiUrl;
constructor(
    private http: HttpClient
  ) { }
public getMarketSummary(): Observable<MarketSummary> { {
    return this.http.get<MarketSummary>(this.apiUrl + encodeUriComponent('/market/get-summary'));
  }
}

Другие подходы к прокси

Apache HTTP и nginx включают прокси-мод. Должна быть возможность создать конфигурацию, чтобы ко всему входящему трафику APIKEY добавлялся к запросу, который затем перенаправлялся на сервер API. CORS также необходимо обрабатывать с помощью конфигурации прокси. Это выходит за рамки данной статьи.

Вывод

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

Первоначально опубликовано на https://rapidapi.com 16 января 2020 г.