Compartir a través de

如何:使用 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 终结点的快速方法。

此示例演示如何创建你自己的 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 请求以检索令牌。

实现 TokenProvider

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

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

以下示例实现向 Azure 函数发出以下请求。 它使用 axios 库发出 HTTP 请求。 你可使用其他库或方法通过服务器代码发出 HTTP 请求。 此特定实现也作为 @fluidframework/azure-client 包的导出提供给你。

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;
    }
}

提高效率并增加错误处理

AzureFunctionTokenProviderTokenProvider 的简单实现,可以从它开始实现自己的自定义令牌提供程序。 若要实现生产就绪的令牌提供程序,应考虑令牌提供程序需要处理的各种故障场景。 例如,AzureFunctionTokenProvider 实现无法处理网络断开连接的情况,因为它不会在客户端缓存令牌。

当容器断开连接时,连接管理器在重新连接到容器之前会尝试从 TokenProvider 获取新的令牌。 当网络断开连接时,fetchOrdererToken 中发出的 API 获取请求将失败并引发不可重试的错误。 这反过来又导致容器被释放,即使重新建立网络连接,也无法重新连接。

此断开连接问题的可能解决方案是将有效令牌缓存在 Window.localStorage 中。 通过令牌缓存,容器将获得有效的存储令牌,而不是在网络断开连接时发出 API 获取请求。 请注意,本地存储的令牌可能在一段时间后过期,你仍需要发出 API 请求来获取新的有效令牌。 在这种情况下,还需要错误处理和重试逻辑,以防在一次尝试失败后容器被释放。

如何选择实现这些改进完全由你和应用程序的要求决定。 请注意,使用 localStorage 令牌解决方案时,应用程序的性能会有所改进,因为在每次调用 getContainer 时都会删除网络请求。

使用 localStorage 之类的令牌缓存可能会带来安全隐患,在决定适合应用程序的解决方案时,你可以自行决定是否使用。 无论是否实现令牌缓存,都应在 fetchOrdererTokenfetchStorageToken 中添加错误处理和重试逻辑,以避免在一次调用失败后即释放容器。 例如,使用 catch 块将 getToken 的调用包装在 try 块中,该块会进行重试并仅在指定的重试次数后引发错误。

另请参阅