用APIM授权实施令牌库

115 阅读4分钟

在这篇文章中,我们将看看最近预览的 Azure API管理(APIM)授权功能,看看如何设置一个React和TypeScript应用程序,使用Dropbox SDK上传文件,而不需要处理OAuth令牌的创建。

什么是APIM授权

在我们深入创建应用程序之前,让我们快速了解一下这个功能是什么。

在一个连接的系统中,能够在不同的软件即服务(SaaS)平台之间进行通信是一项常见的任务,但通常这些平台会使用OAuth2来验证用户身份。这需要进行认证流程,如果你直接使用系统,这很好,但如果是由后台工作处理,比如用定时器触发器运行的Azure函数,那怎么办?那么我们就需要使用其他的认证工作流程,处理令牌的过期,等等。

这可能会导致我们的很多应用代码负责管理和存储令牌。

而这正是Authorizations的用武之地,它是一个管理的令牌库,用于管理你的OAuth2访问令牌。与其说你的应用程序必须进行认证,不如说APIM将代表你处理这个问题。这也意味着你的应用程序可以在一个较低的信任环境中运行,而不是你的应用程序需要知道SaaS提供商的client id/client secret ,它变得不知道,只依赖REST API到API管理,以获得所需的令牌回来。

你可以在APIM的文档中了解更多关于授权的信息。

创建我们的应用程序

我们要创建的应用程序是一个数据输入表单,可以用来在活动中捕获用户信息,一个人输入他们的信息,就会生成一个文件上传到Dropbox,随后可以被我们系统的另一部分摄取。

让我们先用以下方法生成新的应用程序 vite:

npm create vite@latest my-app -- --template react-ts

接下来,我们将开始创建用于数据采集的表单,所以在VS Code(或你选择的任何其他编辑器)中打开my-app 文件夹,我们将在App.tsx 文件中添加一个表单。

const updateField =
  (updater: React.Dispatch<React.SetStateAction<UserInfo>>) =>
  (e: ChangeEvent<HTMLInputElement>) =>
    updater((userInfo) => ({
      ...userInfo,
      [e.target.name]: e.target.value,
    }));

function App() {
  const [userInfo, setUserInfo] = useState<UserInfo>({});
  const [submitting, setSubmitting] = useState(false);

  return (
    <div className="App">
      <header className="App-header">
        <h1>Contoso Lead Capture</h1>
        <form
          action=""
          onSubmit={(e) => (e.preventDefault(), setSubmitting(true))}
        >
          <fieldset>
            <div>
              <label htmlFor="firstName">First name</label>
              <input
                type="text"
                name="firstName"
                id="firstName"
                placeholder="Aaron"
                value={userInfo.firstName}
                onChange={updateField(setUserInfo)}
              />
            </div>
            <div>
              <label htmlFor="lastName">Last name</label>
              <input
                type="text"
                name="lastName"
                id="lastName"
                placeholder="Powell"
                value={userInfo.lastName}
                onChange={updateField(setUserInfo)}
              />
            </div>
          </fieldset>

          <fieldset>
            <div>
              <label htmlFor="email">Email</label>
              <input
                type="email"
                id="email"
                name="email"
                placeholder="foo@email.com"
                value={userInfo.email}
                onChange={updateField(setUserInfo)}
              />
            </div>

            <div>
              <label htmlFor="phone">Phone</label>
              <input
                type="phone"
                id="phone"
                name="phone"
                placeholder="555-555-555"
                value={userInfo.phone}
                onChange={updateField(setUserInfo)}
              />
            </div>
          </fieldset>

          <fieldset>
            <button
              type="submit"
              disabled={
                submitting ||
                !userInfo.firstName ||
                !userInfo.lastName ||
                !userInfo.email ||
                !userInfo.phone
              }
            >
              Submit
            </button>
          </fieldset>
        </form>
      </header>
    </div>
  );
}

我还带来了useState 钩子,这样我们就可以在进行过程中设置各个字段的值,并创建一个类型来代表表单中的数据(并把它放在一个名为types.ts 的新文件中)。

export type UserInfo = {
  firstName?: string;
  lastName?: string;
  email?: string;
  phone?: string;
};

与Dropbox挂钩

现在是时候与Dropbox挂钩了,所以我们需要他们的JavaScript SDK。

npm install --save dropbox

我们将把保存过程放在一个useEffect 钩子里。

  const [dropboxResponse, setDropboxResponse] = useState<
    DropboxSaveResponse | undefined
  >();

  useEffect(() => {
    async function saveToDropbox() {
      const accessToken = "???";

      const dropbox = new Dropbox({ accessToken });

      const contents = `${userInfo.firstName},${userInfo.lastName},${userInfo.email},${userInfo.phone}`;
      const path = `/submissions/${+new Date()}.csv`;

      const response = await dropbox.filesUpload({
        path,
        contents,
      });
      if (response.status !== 200) {
        setDropboxResponse({
          error: true,
          message: "Failed to upload to dropbox",
        });
        return;
      }

      setDropboxResponse({
        error: false,
        message: "Details have been saved. Start again?",
      });
    }

    if (!submitting) {
      return;
    }

    saveToDropbox();
  }, [submitting, userInfo])

我还创建了一个名为DropboxSaveResponse 的类型,以便在钩子上设置。

export type DropboxSaveResponse = {
  error: boolean;
  message: string;
};

我们的代码已经准备好了,好吧,除了一个关键部分--我们如何获得Dropbox SDK的访问令牌?好吧,我们可以启动Dropbox认证流程,但现在每个人都必须能够批准对共享Dropbox账户的访问,这并不理想。值得庆幸的是,这正是APIM授权的目的所在。

用授权设置APIM

我们将使用Azure Portal来部署我们的APIM实例,但作为样本 repo的一部分,我们也提供了一些Bicep模板,所以如果这是你喜欢的方法,请到GitHub repo中查看该指南。另外,如果你只是想进行部署,请点击下面的部署到Azure按钮。

Deploy To Azure

注意:请注意这是预览版,所以在最终发布前可能会有一些变化。

前往Azure门户,创建一个新的APIM实例。

Create an APIM instance

填写必要的字段,然后点击其他屏幕(除了第一个屏幕之外,我们不需要再向APIM资源添加任何东西--除非你想为其他用途配置APIM)。

注意:对于预览,你需要使用开发者定价层。

当资源被创建后,你应该在API分组下看到一个新的**授权(预览)**选项。

Navigate to Authorizations

点击它,我们将看到一个先前创建的授权列表,但由于我们还没有任何授权,我们将从创建按钮开始进行配置。

Authorizations landing view

在这个屏幕上,我们可以配置我们要授权的OAuth2服务,你会在身份提供者列表中看到所有可用的。由于我们使用的是Dropbox,你需要创建一个Dropbox应用程序,并且已经获得了客户ID客户秘密(如果你还没有这样做,请前往Dropbox并进行设置)。

在填写此表格时,请记下提供者名称授权名称,因为我们以后会需要这些。

此外,确保你提供的范围与Dropbox中的范围一致。由于我们要上传文件,我们将需要files.metadata.write files.contents.write files.content.read ,但要根据你的应用需求来匹配这些。

Create an Authorization
在进入下一屏幕之前,复制重定向URL并将其添加到Dropbox应用程序中,这样它就可以在下一步进行验证。

Authenticate the Authorization

在这个过程的第二步,我们需要使用我们创建的OAuth2应用程序对我们的Dropbox应用程序进行APIM认证,所以点击Login with DropBox按钮并遵循它提供的授权工作流程。

设置授权的最后一个阶段是配置授权将使用的访问策略,你可以将其与AAD中的用户/群组联系起来,也可以使用管理身份,如APIM提供的身份。我们将使用管理身份。

Select Managed Identity

在飞入式窗口中选择API管理服务作为管理身份,然后从列出的选项中选择你的服务。

Chose the right Managed Identity

这将弹出主窗口,我们可以完成设置。

Created Authorization

访问我们的令牌

APIM现在作为我们的令牌商店,将根据需要为我们获得新的OAuth2令牌,但我们仍然需要访问它们,为此,我们将在API中创建一个API端点来返回它。前往API部分,我们将手动定义一个HTTP API。

Define a new API

我定义的API将在/token ,由于我们将从另一个虚拟主机调用它,我们需要配置一个CORS策略。我们可以通过点击 "所有操作"并打开代码编辑器来替换默认的策略来做到这一点。

<policies>
    <inbound>
        <cors allow-credentials="false">
            <allowed-origins>
                <origin>*</origin>
            </allowed-origins>
            <allowed-methods>
                <method>GET</method>
                <method>POST</method>
            </allowed-methods>
        </cors>
    </inbound>
    <backend>
        <forward-request />
    </backend>
    <outbound />
    <on-error />
</policies>

这是定义一个入站策略,允许来自所有来源的CORS(你可能想在生产应用中收紧这一点!),并通过所有请求到后端而不受干扰。

现在我们可以创建一个到API的操作,这样我们就可以拿回令牌。

Create an API operation

我把这个操作称为Get Dropbox token ,并在/ URL上做一个HTTPGET ,这是相对于我们定义的API的路径而言的,这意味着这是一个针对/token 的GET请求。

保存好这些后,我们需要定义这个API将做什么。由于我们想访问我们的授权所使用的令牌存储,我们将在入站请求中使用get-authorization-context 策略。

<policies>
<inbound>
    <base />
    <get-authorization-context provider-id="dropbox-demo" authorization-id="auth" context-variable-name="auth-context" ignore-error="false" identity-type="managed" />
    <return-response>
        <set-body>@(((Authorization)context.Variables.GetValueOrDefault(&quot;auth-context&quot;))?.AccessToken)</set-body>
    </return-response>
</inbound>
<backend>
    <base />
</backend>
<outbound>
    <base />
</outbound>
<on-error>
    <base />
</on-error>
</policies>

get-authorization-context 策略需要我们在最初创建授权时设置的两个信息,即提供者的名称,dropbox-demo ,以及授权的名称,auth 。然后,策略将调用我们的令牌存储,抓取令牌,我们使用set-body ,将其设置为主体,在我们的响应中返回。这只是设置一个text/plain 响应,但如果你的情况更喜欢,你可以建立一个JSON有效载荷。

保存策略,点击顶部的测试标签,然后启动请求。

Test our API

成功了!我们可以在HTTP响应中看到,响应体包含我们的OAuth2令牌,我们可以向Dropbox SDK提供。

将其全部连接起来

APIM现在已经配置了所有的授权,所以现在是时候与我们的应用程序集成了。

在React应用程序中,我们将调用我们创建的/token API,你可以从这个命令中获得URL。

SUBSCRIPTION_KEY=$(az rest --method post --url /$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_NAME/subscriptions/master/listSecrets?api-version=2021-08-01 | jq .primaryKey -r)
GATEWAY_URL=$(az apim show --name $APIM_NAME --resource-group $RESOURCE_GROUP --query gatewayUrl --output tsv)

echo "$GATEWAY_URL?dropbox-demo/token?subscription-key=$SUBSCRIPTION_KEY"

注意:我们将在URL中包含subscription key ,该密钥将通过React应用程序暴露出来,因此它可以调用APIM,这意味着你有可能泄露机密。在一个更强大的应用程序中,你可能会包括一个Azure函数,该函数会调用Dropbox,而不是在浏览器中调用,所以你的客户端会POST到Azure函数,它反过来会检索访问令牌并上传文件。但在今天的演示中,我们要把它保留在客户端。

要在我们的React应用程序中使用这个,请在工作区的根部创建一个.env 文件,然后像这样添加进去。

VITE_APIM_ENDPOINT=<...>

现在我们可以回到我们的App.tsx ,更新这一行。

const accessToken = "???";

为了。

const accessTokenResponse = await fetch(import.meta.env.VITE_APIM_ENDPOINT);
const accessToken = await accessTokenResponse.text();

npm run dev 启动应用程序,在表格中填写数据并点击提交--你会看到对APIM的调用,获得访问令牌,然后它被提供给Dropbox SDK,将文件上传到Dropbox。

Sample app in action

总结

你已经了解了我们为API管理添加的新功能--授权。

在这篇文章中,我们已经了解了如何在APIM中设置授权,在这种情况下,我们使用Dropbox,将APIM连接到我们的Dropbox应用程序,它可以代表我们请求OAuth2访问令牌。然后我们在APIM中创建了一个策略,它将通过我们可以进行的API调用返回访问令牌,而不是我们必须从头开始建立自己的API。

我们还建立了一个React应用程序,它可以调用我们在APIM中创建的API,从令牌存储中取回Dropbox访问令牌,将其提供给Dropbox SDK,然后将文件上传到Dropbox,所有这些都不需要客户端自己进行OAuth2流程。

你可以在GitHub上找到这个应用程序的样本,包括用于配置APIM的脚本和Blazor/C#版本。要了解更多关于Blazor版本的信息,请看我的同事Justin Yoo写的篇文章。

不要忘了阅读API管理中的授权文档,并让我们知道你会发现它有哪些用途。