Использование внешних служб из службы управления API Azure

ОБЛАСТЬ ПРИМЕНЕНИЯ: все уровни управления API

Политики, доступные в службе управления API Azure, позволяют выполнять множество полезных задач исключительно на основе входящих запросов, исходящих ответов и сведений о базовой конфигурации. Однако возможность взаимодействия с внешними службами управления из политик управления API предоставляет гораздо больше преимуществ.

В предыдущих статьях вы узнали, как взаимодействовать со службой Центров событий Azure для ведения журнала, мониторинга и аналитики. В этой статье вы узнаете о политиках, которые позволяют работать с любой внешней HTTP-службой. Эти политики можно использовать для запуска удаленных событий или для получения данных, которые определенным образом применяются для обработки исходного запроса и ответа.

Отправка однонаправленного запроса

Возможно, самым простым внешним взаимодействием является одноразовый стиль запроса, который позволяет внешней службе получать уведомления о каком-то важном событии. Политику choose потока управления можно использовать для обнаружения любого интересующего вас условия. Если условие выполняется, можно сделать внешний HTTP-запрос, используя политику отправки одностороннего запроса. Этот запрос может быть в системе обмена сообщениями, например Hipchat или Slack, или в API почты, например SendGrid или MailChimp, или для критически важных инцидентов поддержки, таких как PagerDuty. Все эти системы обмена сообщениями обладают простыми API-интерфейсами HTTP, которые могут быть вызваны.

Оповещения с использованием Slack

В следующем примере демонстрируется отправка сообщения в чат Slack, если код состояния HTTP-ответа больше или равен 500. Ошибка диапазона 500 указывает на проблему с внутренним API, которую клиент API не может устранить самостоятельно. Обычно это требует какого-либо вмешательства в части части управления API.

<choose>
  <when condition="@(context.Response.StatusCode >= 500)">
    <send-one-way-request mode="new">
      <set-url>https://hooks.slack.com/services/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</set-url>
      <set-method>POST</set-method>
      <set-body>@{
        return new JObject(
          new JProperty("username","APIM Alert"),
          new JProperty("icon_emoji", ":ghost:"),
          new JProperty("text", String.Format("{0} {1}\nHost: {2}\n{3} {4}\n User: {5}",
            context.Request.Method,
            context.Request.Url.Path + context.Request.Url.QueryString,
            context.Request.Url.Host,
            context.Response.StatusCode,
            context.Response.StatusReason,
            context.User.Email
          ))
        ).ToString();
      }</set-body>
    </send-one-way-request>
  </when>
</choose>

В системе Slack используется понятие входящих веб-привязок. При настройке входящего веб-перехватчика Slack создает специальный URL-адрес, который позволяет выполнять базовый запрос POST и передавать сообщение в канал Slack. Создаваемый текст JSON основан на формате, определенном системой Slack.

Снимок экрана вебхука Slack.

Достаточно ли хороша стратегия «выпустил и забыл»?

Использование запросов в стиле «пуск и забыть» связано с некоторыми компромиссами. Если по какой-то причине запрос завершается ошибкой, сообщение об ошибке не сообщается. В этой ситуации сложность вторичной системы отчетности о сбоях и дополнительная затратность на производительность из-за ожидания ответа не являются оправданными. В сценариях, где необходимо проверить ответ, лучше использовать политику отправки запросов .

Отправить запрос

Политика send-request позволяет использовать внешнюю службу для выполнения сложных действий по обработке и возврату данных в службу управления API для дальнейшей обработки политики.

Авторизация маркеров ссылок

Основная задача API управления состоит в защите серверных ресурсов. Если сервер авторизации, используемый вашим API, создает JSON Web Tokens (JWT) в рамках своего потока OAuth2, как это делает Microsoft Entra ID, можно использовать политику validate-jwt или политику validate-azure-ad-token для проверки действительности токена. Некоторые серверы авторизации создают то, что называются маркерами ссылок , которые не могут быть проверены без обратного вызова на сервер авторизации.

Стандартизованный самоанализ

В прошлом не существовало стандартизированного способа проверки референсного токена с сервером авторизации. Тем не менее, группа задач по разработке Интернета (IETF) недавно опубликовала предлагаемый стандарт RFC 7662 , который определяет, как сервер ресурсов может проверить допустимость токена.

Извлечение токена

Первым действием является извлечение маркера из заголовка авторизации. Значение заголовка должно быть отформатировано с помощью схемы авторизации Bearer — согласно RFC 6750 сначала указывается одиночный пробел, за которым следует маркер авторизации. К сожалению, существуют случаи, когда схема авторизации опускается. Чтобы учитывать это упущение при синтаксическом анализе, управление API разбивает значение заголовка на пробел и выбирает последнюю строку из возвращаемого массива строк. Этот метод предоставляет обходной путь для плохо отформатированных заголовков авторизации.

<set-variable name="token" value="@(context.Request.Headers.GetValueOrDefault("Authorization","scheme param").Split(' ').Last())" />

Выполнение запроса на проверку

После того как служба управления API получила маркер авторизации, она может выполнить запрос на проверку маркера. Согласно RFC 7662, этот процесс называется интроспекцией, и требуется, чтобы вы отправили HTML-форму в ресурс интроспекции с помощью POST. HTML-форма должна содержать по крайней мере пару "ключ-значение" с ключом token. Этот запрос на сервер авторизации также должен быть аутентифицирован, чтобы гарантировать, что вредоносные клиенты не могут искать действительные токены.

<send-request mode="new" response-variable-name="tokenstate" timeout="20" ignore-error="true">
  <set-url>https://microsoft-apiappec990ad4c76641c6aea22f566efc5a4e.azurewebsites.net/introspection</set-url>
  <set-method>POST</set-method>
  <set-header name="Authorization" exists-action="override">
    <value>basic dXNlcm5hbWU6cGFzc3dvcmQ=</value>
  </set-header>
  <set-header name="Content-Type" exists-action="override">
    <value>application/x-www-form-urlencoded</value>
  </set-header>
  <set-body>@($"token={(string)context.Variables["token"]}")</set-body>
</send-request>

Проверка ответа

Атрибут response-variable-name используется для предоставления доступа к возвращаемого ответа. Имя, определенное в этом свойстве, можно использовать в качестве ключа к словарю context.Variables для доступа к объекту IResponse.

Из объекта ответа можно извлечь его тело, а RFC 7622 указывает, что управление API ожидает ответ в виде объекта JSON, который должен содержать, по крайней мере, свойство с именем active, являющееся логическим значением. Если active задано значение true, маркер считается допустимым.

Кроме того, если сервер авторизации не включает "active" поле, указывающее, является ли маркер допустимым, используйте средство http-клиента, например curl для определения свойств, заданных в допустимом токене. Например, если допустимый ответ маркера содержит свойство с именем "expires_in", проверьте, существует ли это имя свойства в ответе сервера авторизации таким образом:

<when condition="@(((IResponse)context.Variables["tokenstate"]).Body.As<JObject>().Property("expires_in") == null)">

Неудача при отправке отчета

Для обнаружения недопустимого маркера используется политика <choose>, и если маркер является недопустимым, возвращается ответ 401.

<choose>
  <when condition="@((bool)((IResponse)context.Variables["tokenstate"]).Body.As<JObject>()["active"] == false)">
    <return-response response-variable-name="existing response variable">
      <set-status code="401" reason="Unauthorized" />
      <set-header name="WWW-Authenticate" exists-action="override">
        <value>Bearer error="invalid_token"</value>
      </set-header>
    </return-response>
  </when>
</choose>

Как указано в RFC 6750, в котором описывается, как должны использоваться токены bearer, система управления API также возвращает заголовок ответа с кодом 401 WWW-Authenticate. WWW-Authenticate предназначен для предоставления клиенту сведений о создании надлежащим образом авторизованного запроса. Благодаря широкому спектру подходов к платформе OAuth2 трудно сообщить все необходимые сведения. К счастью, существуют возможности, позволяющие клиентам узнать о правильной авторизации запросов к серверу ресурсов.

Окончательное решение

В итоге вы получите следующую политику:

<inbound>
  <!-- Extract Token from Authorization header parameter -->
  <set-variable name="token" value="@(context.Request.Headers.GetValueOrDefault("Authorization","scheme param").Split(' ').Last())" />

  <!-- Send request to Token Server to validate token (see RFC 7662) -->
  <send-request mode="new" response-variable-name="tokenstate" timeout="20" ignore-error="true">
    <set-url>https://microsoft-apiappec990ad4c76641c6aea22f566efc5a4e.azurewebsites.net/introspection</set-url>
    <set-method>POST</set-method>
    <set-header name="Authorization" exists-action="override">
      <value>basic dXNlcm5hbWU6cGFzc3dvcmQ=</value>
    </set-header>
    <set-header name="Content-Type" exists-action="override">
      <value>application/x-www-form-urlencoded</value>
    </set-header>
    <set-body>@($"token={(string)context.Variables["token"]}")</set-body>
  </send-request>

  <choose>
    <!-- Check active property in response -->
    <when condition="@((bool)((IResponse)context.Variables["tokenstate"]).Body.As<JObject>()["active"] == false)">
      <!-- Return 401 Unauthorized with http-problem payload -->
      <return-response response-variable-name="existing response variable">
        <set-status code="401" reason="Unauthorized" />
        <set-header name="WWW-Authenticate" exists-action="override">
          <value>Bearer error="invalid_token"</value>
        </set-header>
      </return-response>
    </when>
  </choose>
  <base />
</inbound>

Этот пример является лишь одним из многих, демонстрирующих, как send-request политику можно использовать для интеграции полезных внешних служб в процесс запросов и ответов, поступающих через службу управления API.

Структура ответа

Политику send-request можно использовать для повышения первичного запроса к серверной системе, как показано в предыдущем примере, или его можно использовать для полной замены внутреннего вызова. С помощью этой методики можно легко создавать сложные ресурсы, которые объединяются из нескольких разных систем.

Создание панели мониторинга

Иногда возникает потребность предоставить информацию из нескольких серверных систем, например, для использования в панели мониторинга. Ключевые показатели производительности (ключевые показатели эффективности) приходят из всех разных внутренних серверов, но вы предпочитаете не предоставлять прямой доступ к ним. Тем не менее, было бы хорошо, если бы все сведения можно было получить в одном запросе. Возможно, некоторые фоновые данные потребуется сначала разделить на части и немного очистить. Способность кэшировать составной ресурс будет полезным способом уменьшить нагрузку на серверную часть, так как у пользователей есть привычка нажимать клавишу F5, чтобы узнать, могут ли измениться их неэффективные метрики.

Подделка ресурса

Первым шагом в создании ресурса панели мониторинга является настройка новой операции на портале Azure. Эта операция плейсхолдера настраивает политику композиции для построения динамического ресурса.

Снимок экрана: новая операция панели мониторинга, настроенная на портале Azure.

Выполнение запросов

После создания операции можно настроить политику специально для этой операции.

Снимок экрана: экран области политики.

Первым шагом является извлечение всех параметров запроса из входящего запроса, чтобы их можно было переслать в серверную часть. В этом примере на панели мониторинга сведения отображаются на основе определенного периода времени, поэтому здесь присутствуют параметры fromDate и toDate. С помощью политики set-variable можно извлечь сведения из URL-адреса запроса.

<set-variable name="fromDate" value="@(context.Request.Url.Query["fromDate"].Last())">
<set-variable name="toDate" value="@(context.Request.Url.Query["toDate"].Last())">

После получения этой информации можно выполнять запросы ко всем серверным системам. Каждый запрос создает новый URL-адрес со сведениями о параметрах, вызывает соответствующий сервер и сохраняет ответ в переменной контекста.

<send-request mode="new" response-variable-name="revenuedata" timeout="20" ignore-error="true">
  <set-url>@($"https://accounting.acme.com/salesdata?from={(string)context.Variables["fromDate"]}&to={(string)context.Variables["fromDate"]}")</set-url>
  <set-method>GET</set-method>
</send-request>

<send-request mode="new" response-variable-name="materialdata" timeout="20" ignore-error="true">
  <set-url>@($"https://inventory.acme.com/materiallevels?from={(string)context.Variables["fromDate"]}&to={(string)context.Variables["fromDate"]}")</set-url>
  <set-method>GET</set-method>
</send-request>

<send-request mode="new" response-variable-name="throughputdata" timeout="20" ignore-error="true">
  <set-url>@($"https://production.acme.com/throughput?from={(string)context.Variables["fromDate"]}&to={(string)context.Variables["fromDate"]}")</set-url>
  <set-method>GET</set-method>
</send-request>

<send-request mode="new" response-variable-name="accidentdata" timeout="20" ignore-error="true">
  <set-url>@($"https://production.acme.com/accidentdata?from={(string)context.Variables["fromDate"]}&to={(string)context.Variables["fromDate"]}")</set-url>
  <set-method>GET</set-method>
</send-request>

Управление API отправляет эти запросы последовательно.

Реагирование

Для формирования составного ответа можно использовать политику возврата ответа. Элемент set-body может применять выражение для создания нового JObject со всеми представлениями компонентов, внедренными в качестве свойств.

<return-response response-variable-name="existing response variable">
  <set-status code="200" reason="OK" />
  <set-header name="Content-Type" exists-action="override">
    <value>application/json</value>
  </set-header>
  <set-body>
    @(new JObject(new JProperty("revenuedata",((IResponse)context.Variables["revenuedata"]).Body.As<JObject>()),
                  new JProperty("materialdata",((IResponse)context.Variables["materialdata"]).Body.As<JObject>()),
                  new JProperty("throughputdata",((IResponse)context.Variables["throughputdata"]).Body.As<JObject>()),
                  new JProperty("accidentdata",((IResponse)context.Variables["accidentdata"]).Body.As<JObject>())
                  ).ToString())
  </set-body>
</return-response>

Законченная политика выглядит следующим образом:

<policies>
  <inbound>
    <set-variable name="fromDate" value="@(context.Request.Url.Query["fromDate"].Last())">
    <set-variable name="toDate" value="@(context.Request.Url.Query["toDate"].Last())">

    <send-request mode="new" response-variable-name="revenuedata" timeout="20" ignore-error="true">
      <set-url>@($"https://accounting.acme.com/salesdata?from={(string)context.Variables["fromDate"]}&to={(string)context.Variables["fromDate"]}")"</set-url>
      <set-method>GET</set-method>
    </send-request>

    <send-request mode="new" response-variable-name="materialdata" timeout="20" ignore-error="true">
      <set-url>@($"https://inventory.acme.com/materiallevels?from={(string)context.Variables["fromDate"]}&to={(string)context.Variables["fromDate"]}")"</set-url>
      <set-method>GET</set-method>
    </send-request>

    <send-request mode="new" response-variable-name="throughputdata" timeout="20" ignore-error="true">
      <set-url>@($"https://production.acme.com/throughput?from={(string)context.Variables["fromDate"]}&to={(string)context.Variables["fromDate"]}")"</set-url>
      <set-method>GET</set-method>
    </send-request>

    <send-request mode="new" response-variable-name="accidentdata" timeout="20" ignore-error="true">
      <set-url>@($"https://production.acme.com/accidentdata?from={(string)context.Variables["fromDate"]}&to={(string)context.Variables["fromDate"]}")"</set-url>
      <set-method>GET</set-method>
    </send-request>

    <return-response response-variable-name="existing response variable">
      <set-status code="200" reason="OK" />
      <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
      </set-header>
      <set-body>
        @(new JObject(new JProperty("revenuedata",((IResponse)context.Variables["revenuedata"]).Body.As<JObject>()),
                      new JProperty("materialdata",((IResponse)context.Variables["materialdata"]).Body.As<JObject>()),
                      new JProperty("throughputdata",((IResponse)context.Variables["throughputdata"]).Body.As<JObject>()),
                      new JProperty("accidentdata",((IResponse)context.Variables["accidentdata"]).Body.As<JObject>())
        ).ToString())
      </set-body>
    </return-response>
  </inbound>
  <backend>
    <base />
  </backend>
  <outbound>
    <base />
  </outbound>
</policies>

Итоги

Служба управления API Azure предоставляет гибкие политики, выборочно применяемые к HTTP-трафику, и позволяет формировать серверные службы. Если вы хотите усовершенствовать имеющийся шлюз API за счет функций оповещения, проверки или создать новые сложные ресурсы на основе нескольких серверных служб, send-request и связанные политики предоставят вам широкий диапазон новых возможностей.