你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

通过 Azure API 管理服务使用外部服务

适用于:所有 API 管理层级

Azure API 管理服务中的策略可以单纯根据传入的请求、传出的响应以及基本配置信息执行多种不同的有用工作。 但是,如果能够与 API 管理策略中的外部服务进行交互,则可以使更多的想法成为可能。

在前面的文章中,你已了解如何与 Azure 事件中心服务进行交互,以便进行日志记录、监视和分析。 本文演示可用来与基于 HTTP 的任何外部服务进行交互的策略。 这些策略可用于触发远程事件,或检索可用于以某种方式处理原始请求和响应的信息。

Send-One-Way-Request

最简单的外部交互可能是采用发出后便不管风格的请求,允许外部服务接收关于某些重要事件的通知。 控制流策略 choose 可用于检测你感兴趣的任何类型的条件。 如果条件满足,可以使用 send-one-way-request 策略发起外部 HTTP 请求。 此请求可以是对消息传送系统(例如 Hipchat 或 Slack)的请求,也可能是对邮件 API(例如 SendGrid 或 MailChimp)的请求,或者是针对某些例如 PagerDuty 的重大支持事件的请求。 所有这些消息传送系统都提供可供调用的简单 HTTP API。

使用 Slack 发出警报

以下示例演示当 HTTP 响应状态代码大于或等于 500 时如何将消息发送到 Slack 聊天室。 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 具有入站 Web Hook 的概念。 当它配置入站 Web 挂钩时,Slack 会生成一个特殊的 URL,该 URL 允许你执行基本的 POST 请求并将消息传递到 Slack 通道。 创建的 JSON 主体基于 Slack 定义的格式。

Slack Webhook 的屏幕截图。

即发即弃是否足够好?

使用请求的即发即弃形式有一些特定的权衡取舍。 如果出于某种原因,请求失败,则不会报告失败。 在这种情况下,不需要辅助故障报告系统的复杂性和等待响应的额外性能成本。 对于检查响应至关重要的方案,则 发送请求 策略是更好的选择。

Send-Request

send-request 策略能够使用外部服务来执行复杂的处理函数,并将数据返回到 API 管理服务,此服务可用于进一步处理策略。

授权引用令牌

API 管理的主要功能是保护后端资源。 如果 API 使用的授权服务器在其 OAuth2 流中创建 JSON Web 令牌(JWT), 就像 Microsoft Entra ID 一样,则可以使用 validate-jwt 策略或 validate-azure-ad-token 策略来验证令牌的有效性。 某些授权服务器创建称为引用令牌的内容,而这些令牌在不进行回调到授权服务器的情况下无法验证。

标准化自检

过去,没有使用授权服务器验证引用令牌的标准化方法。 但是,Internet 工程工作队(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 管理还返回了 WWW-Authenticate 标头以及 401 响应。 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 可用于增强对后端系统的主要请求,如上一示例中所示,或者可用于完全替换后端调用。 使用此技术可以轻松创建聚合自多个不同系统的复合资源。

构建仪表板

有时我们希望能够公开多个后端系统中的信息,例如,驱动仪表板。 关键绩效指标(KPI)来自所有不同的后端,但你不希望提供对这些指标的直接访问。 不过,如果所有信息都可以在单个请求中检索,那会很好。 也许有些后端信息需要进行某种切片和细分,需要先稍微处理一下! 能够缓存该复合资源是减少后端负载的有用方法,因为你知道用户有锤击 F5 键的习惯,以便查看其性能不佳的指标是否会发生变化。

伪装资源

构建仪表板资源的第一步是在 Azure 门户中配置新的操作。 这是占位符操作,用于配置编写策略以构建动态资源。

显示 Azure 门户中正在配置的新仪表板操作的屏幕截图。

发出请求

创建操作后,可以专门为该操作配置策略。

显示“策略范围”屏幕的屏幕截图。

第一个步骤是提取来自传入请求的任何查询参数,以便将其转发到后端。 在本示例中,仪表板每隔一段时间显示信息,因此具有 fromDatetoDate 参数。 可以使用 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 管理按顺序发送这些请求。

响应

若要构造复合响应,可以使用 return-response 策略。 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>

摘要

Azure API 管理服务提供可根据需要应用到 HTTP 流量的灵活策略,并支持后端服务的组合。 不管是要使用警报、校验、验证功能还是基于多个后端服务创建新的复合资源来增强 API 网关,send-request 和相关策略都能使这种想法成为可能。