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

如何:使用 Azure 函数编写 TokenProvider

注意

此预览版在提供时没有附带服务级别协议,不建议将其用于生产工作负荷。 某些功能可能不受支持或者受限。

Fluid Framework 中,TokenProvider 负责创建和签署令牌,@fluidframework/azure-client 使用这些令牌向 Azure Fluid Relay 服务发出请求。 Fluid Framework 提供一个简单但不安全的 TokenProvider 用于开发目的,并且形象地将其命名为 InsecureTokenProvider。 每个 Fluid 服务都必须基于特定服务的身份验证和安全考虑实现一个自定义 TokenProvider。

你创建的每个 Azure Fluid Relay 资源都分配有租户 ID 及其独有的租户密钥。 该密钥是一种共享机密。 你的应用/服务知道这一点,Azure Fluid Relay 服务也知道。 TokenProvider 必须知道密钥才能签署请求,但密钥不能包含在客户端代码中。

实现 Azure 函数,以对令牌进行签名

生成安全令牌提供程序的一个选项是:创建 HTTPS 终结点并创建 TokenProvider 实现,用于向该终结点发送经过身份验证的 HTTPS 请求来检索令牌。 这样,便可将租户密钥存储在安全的位置,例如 Azure Key Vault

完整的解决方案包含两个部分:

  1. HTTPS 终结点,用于接受请求并返回 Azure Fluid Relay 令牌。
  2. ITokenProvider 实现,用于接受终结点的 URL,然后请求该终结点检索令牌。

使用 Azure Functions 为 TokenProvider 创建终结点

使用 Azure Functions 是创建此类 HTTPS 终结点的快速方法。 以下示例在名为 AzureFunctionTokenProvider 的类中实现该模式。 它接受 Azure 函数的 URL、userIduserName

此示例演示如何创建你自己的 HTTPTrigger Azure 函数,以便通过传入租户密钥来提取令牌。

import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { ScopeType } from "@fluidframework/azure-client";
import { generateToken } from "@fluidframework/azure-service-utils";

// NOTE: retrieve the key from a secure location.
const key = "myTenantKey";

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    // tenantId, documentId, userId and userName are required parameters
    const tenantId = (req.query.tenantId || (req.body && req.body.tenantId)) as string;
    const documentId = (req.query.documentId || (req.body && req.body.documentId)) as string | undefined;
    const userId = (req.query.userId || (req.body && req.body.userId)) as string;
    const userName = (req.query.userName || (req.body && req.body.userName)) as string;
    const scopes = (req.query.scopes || (req.body && req.body.scopes)) as ScopeType[];

    if (!tenantId) {
        context.res = {
            status: 400,
            body: "No tenantId provided in query params",
        };
        return;
    }

    if (!key) {
        context.res = {
            status: 404,
            body: `No key found for the provided tenantId: ${tenantId}`,
        };
        return;
    }

    let user = { name: userName, id: userId };

    // Will generate the token and returned by an ITokenProvider implementation to use with the AzureClient.
    const token = generateToken(
        tenantId,
        documentId,
        key,
        scopes ?? [ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
        user
    );

    context.res = {
        status: 200,
        body: token
    };
};

export default httpTrigger;

@fluidframework/azure-service-utils 包中发现的 generateToken 函数会为给定用户生成一个使用租户密钥签署的令牌。 此方法可将令牌返回到客户端,而无需公开机密。 相反,令牌是使用机密在服务器端生成的,用于提供对给定文档的范围内访问。 以下示例 ITokenProvider 向此 Azure 函数发送 HTTP 请求以检索令牌。

部署 Azure Function

你可通过几种方式部署 Azure Functions。 要详细了解如何部署 Azure Functions,请参阅 Azure Functions 文档中的“部署”部分。

实现 TokenProvider

可通过多种方式实现 TokenProvider,但必须实现两个单独的 API 调用:fetchOrdererTokenfetchStorageToken。 这些 API 分别负责提取 Fluid Orderer 和存储服务的令牌。 这两个函数都返回表示令牌值的 TokenResponse 对象。 Fluid Framework 运行时根据需要调用这两个 API 来检索令牌。 请注意,虽然应用程序代码仅使用一个服务终结点与 Azure Fluid Relay 服务建立连接,但 Azure 客户端与该服务在内部结合使用会将一个终结点转换为 orderer 和存储终结点对。 这两个终结点将从该点开始用于该会话,这就是需要实现两个单独的函数来提取令牌的原因,每个函数一个。

为了确保租户密钥的安全,它存储在安全的后端位置,并且只能从 Azure 函数内部访问。 要检索令牌,需要向已部署的 Azure 函数发出 GETPOST 请求,并提供 tenantIDdocumentId,以及 userID/userName。 Azure 函数负责租户 ID 和租户密钥之间的映射,以适当地生成和签署令牌。

下面的示例实现使用 axios 库发出 HTTP 请求。 你可使用其他库或方法通过服务器代码发出 HTTP 请求。

import { ITokenProvider, ITokenResponse } from "@fluidframework/routerlicious-driver";
import axios from "axios";
import { AzureMember } from "./interfaces";

/**
 * Token Provider implementation for connecting to an Azure Function endpoint for
 * Azure Fluid Relay token resolution.
 */
export class AzureFunctionTokenProvider implements ITokenProvider {
    /**
     * Creates a new instance using configuration parameters.
     * @param azFunctionUrl - URL to Azure Function endpoint
     * @param user - User object
     */
    constructor(
        private readonly azFunctionUrl: string,
        private readonly user?: Pick<AzureMember, "userId" | "userName" | "additionalDetails">,
    ) { }

    public async fetchOrdererToken(tenantId: string, documentId?: string): Promise<ITokenResponse> {
        return {
            jwt: await this.getToken(tenantId, documentId),
        };
    }

    public async fetchStorageToken(tenantId: string, documentId: string): Promise<ITokenResponse> {
        return {
            jwt: await this.getToken(tenantId, documentId),
        };
    }

    private async getToken(tenantId: string, documentId: string | undefined): Promise<string> {
        const response = await axios.get(this.azFunctionUrl, {
            params: {
                tenantId,
                documentId,
                userId: this.user?.userId,
                userName: this.user?.userName,
                additionalDetails: this.user?.additionalDetails,
            },
        });
        return response.data as string;
    }
}

请参阅