Azure API 管理:使用 OAuth 保护网关和后端之间的连接?

Azure API Management: Using OAuth to secure a connection between the gateway and a backend?

我们有一个受标准 OAuth 凭据流保护的现有后端。我们正在移动所有流量以通过 Azure API 网关,并发现以下策略使用 OAuth(来源:Use OAuth2 for authorization between the gateway and a backend)。

<!-- The policy defined in this file provides an example of using OAuth2 for authorization between the gateway and a backend. -->
<!-- It shows how to obtain an access token from AAD and forward it to the backend. -->

<!-- Send request to AAD to obtain a bearer token -->
<!-- Parameters: authorizationServer - format https://login.windows.net/TENANT-GUID/oauth2/token -->
<!-- Parameters: scope - a URI encoded scope value -->
<!-- Parameters: clientId - an id obtained during app registration -->
<!-- Parameters: clientSecret - a URL encoded secret, obtained during app registration -->

<!-- Copy the following snippet into the inbound section. -->

<policies>
  <inbound>
    <base />
      <send-request ignore-error="true" timeout="20" response-variable-name="bearerToken" mode="new">
        <set-url>{{authorizationServer}}</set-url>
        <set-method>POST</set-method>
        <set-header name="Content-Type" exists-action="override">
          <value>application/x-www-form-urlencoded</value>
        </set-header>
        <set-body>
          @{
          return "client_id={{clientId}}&resource={{scope}}&client_secret={{clientSecret}}&grant_type=client_credentials";
          }
        </set-body>
      </send-request>

      <set-header name="Authorization" exists-action="override">
        <value>
          @("Bearer " + (String)((IResponse)context.Variables["bearerToken"]).Body.As<JObject>()["access_token"])
      </value>
      </set-header>

      <!--  Don't expose APIM subscription key to the backend. -->
      <set-header exists-action="delete" name="Ocp-Apim-Subscription-Key"/>
  </inbound>
  <backend>
    <base />
  </backend>
  <outbound>
    <base />
  </outbound>
  <on-error>
    <base />
  </on-error>
</policies>

但是,该政策似乎并未重复使用令牌,因此每次调用都会获取一个新令牌。这不是最佳的,主要是因为性能,还因为我们与 Auth0 的协议对这些调用的数量有限制。

在网关和后端之间进行调用时,是否有任何方法可以重用令牌(如果它仍然有效)?

尝试使用缓存存储值和缓存获取值将令牌存储在缓存中。如果您事先检查令牌,则可以将其过期时间作为 ttl 放入缓存中。只要确保有回退逻辑,以防缓存令牌不起作用。

没有简单的方法来重用策略,因此重试部分可能看起来很麻烦。但是只有当你想重试对缓存令牌的 401 响应调用时才有必要。

<policies>
    <inbound>
        <base />
        <cache-lookup-value key="bearerToken" variable-name="bearerToken" />
        <choose>
            <when condition="@(!context.Variables.ContainsKey("bearerToken"))">
                <send-request ignore-error="true" timeout="20" response-variable-name="bearerToken" mode="new">
                    <set-url>{{authorizationServer}}</set-url>
                    <set-method>POST</set-method>
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/x-www-form-urlencoded</value>
                    </set-header>
                    <set-body>@("client_id={{clientId}}&resource={{scope}}&client_secret={{clientSecret}}&grant_type=client_credentials")</set-body>
                </send-request>
                <set-variable name="bearerToken" value="@((string)((IResponse)context.Variables["bearerToken"]).Body.As<JObject>()["access_token"])" />
                <cache-store-value key="bearerToken" value="@((string)context.Variables["bearerToken"])" duration="60" />
                <set-variable name="cachedToken" value="@(false)" />
            </when>
            <otherwise>
                <set-variable name="cachedToken" value="@(true)" />
            </otherwise>
        </choose>

        <!--  Don't expose APIM subscription key to the backend. -->
        <set-header exists-action="delete" name="Ocp-Apim-Subscription-Key"/>
    </inbound>
    <backend>
        <retry condition="@((bool)context.Variables["cachedToken"] && context.Response.StatusCode == 401)" count="1" interval="0" first-fast-retry="true">
            <choose>
                <when condition="@(context.Response.StatusCode == 401)">
                    <send-request ignore-error="true" timeout="20" response-variable-name="bearerToken" mode="new">
                        <set-url>{{authorizationServer}}</set-url>
                        <set-method>POST</set-method>
                        <set-header name="Content-Type" exists-action="override">
                            <value>application/x-www-form-urlencoded</value>
                        </set-header>
                        <set-body>@("client_id={{clientId}}&resource={{scope}}&client_secret={{clientSecret}}&grant_type=client_credentials")</set-body>
                    </send-request>
                    <set-variable name="bearerToken" value="@((string)((IResponse)context.Variables["bearerToken"]).Body.As<JObject>()["access_token"])" />
                    <cache-store-value key="bearerToken" value="@((string)context.Variables["bearerToken"])" duration="60" />
                    <set-variable name="cachedToken" value="@(false)" />
                </when>
            </choose>

            <set-header name="Authorization" exists-action="override">
                <value>@("Bearer " + (string)context.Variables["bearerToken"])</value>
            </set-header>

            <forward-request />
        </retry>
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

最干净和最简单的方法如下,它还利用 'expires_in' 来设置缓存持续时间:

<policies>
<inbound>
    <base />
    <cache-lookup-value key="cachedToken" variable-name="access_token" />
    <choose>
        <when condition="@(!context.Variables.ContainsKey("access_token"))">
            <send-request ignore-error="true" timeout="20" response-variable-name="response" mode="new">
                <set-url>{{authorizationServer}}</set-url>
                <set-method>POST</set-method>
                <set-header name="Content-Type" exists-action="override">
                    <value>application/x-www-form-urlencoded</value>
                </set-header>
                <set-body>@("client_id={{clientId}}&resource={{scope}}&client_secret={{clientSecret}}&grant_type=client_credentials")</set-body>
            </send-request>
            <set-variable name="responseObject" value="@(((IResponse)context.Variables["response"]).Body.As<JObject>())" />
            <set-variable name="access_token" value="@((string)((JObject)context.Variables["responseObject"])["access_token"])" />
            <set-variable name="expires_in" value="@((int)((JObject)context.Variables["responseObject"])["expires_in"])" />
            <cache-store-value key="cachedToken" value="@((string)context.Variables["access_token"])" duration="@(((int)context.Variables["expires_in"]) - 10)" />
        </when>
    </choose>
    <set-header name="Authorization" exists-action="override">
        <value>@("Bearer " + (string)context.Variables["access_token"])</value>
    </set-header>
    <!--  Don't expose APIM subscription key to the backend. -->
    <set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" />
</inbound>
<backend>
    <base />
</backend>
<outbound>
    <base />
</outbound>
<on-error>
    <base />
</on-error>