방법: Azure Function을 사용하여 TokenProvider 작성

참고 항목

이 미리 보기 버전은 서비스 수준 계약 없이 제공되며 프로덕션 워크로드에는 사용하지 않는 것이 좋습니다. 특정 기능이 지원되지 않거나 기능이 제한될 수 있습니다.

Fluid Framework에서 TokenProviders는 @fluidframework/azure-client가 Azure Fluid Relay 서비스로 요청하는 데 사용하는 토큰을 만들고 서명하는 작업을 담당합니다. Fluid Framework는 개발 목적으로 간단하고 안전하지 않은 TokenProvider를 제공하며, InsecureTokenProvider라고도 합니다. 각 Fluid 서비스는 특정 서비스의 인증 및 보안 고려 사항에 따라 사용자 지정 TokenProvider를 구현해야 합니다.

사용자가 만드는 각 Azure Fluid Relay 리소스에는 테넌트 ID와 고유한 테넌트 비밀 키가 할당됩니다. 비밀 키는 공유 비밀입니다. 앱/서비스는 이를 알고 있으며 Azure Fluid Relay 서비스도 이를 알고 있습니다. TokenProviders는 요청에 서명하기 위해 비밀 키를 알고 있어야 하지만 비밀 키는 클라이언트 코드에 포함할 수 없습니다.

토큰 서명을 위한 Azure Function 구현

보안 토큰 공급자를 빌드하는 한 가지 옵션은 HTTPS 엔드포인트를 만들고 토큰을 검색하기 위해 해당 엔드포인트에 인증된 HTTPS 요청을 만드는 TokenProvider 구현을 만드는 것입니다. 이 경로를 사용하면 Azure Key Vault 같은 안전한 위치에 테넌트 비밀 키를 저장할 수 있습니다.

전체 솔루션은 다음 두 가지 부분을 포함합니다.

  1. 요청을 수락하고 Azure Fluid Relay 토큰을 반환하는 HTTPS 엔드포인트.
  2. 엔드포인트에 대한 URL을 허용한 다음 해당 엔드포인트에 토큰을 검색하도록 요청하는 ITokenProvider 구현.

Azure Functions를 사용하여 TokenProvider에 대한 엔드포인트 만들기

Azure Functions를 사용하면 이러한 HTTPS 엔드포인트를 빠르게 만들 수 있습니다.

이 예제에서는 테넌트 키를 전달하여 토큰을 가져오는 고유한 HTTPTrigger Azure Function 을 만드는 방법을 보여 줍니다.

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 Function에 대한 HTTP 요청을 수행하여 토큰을 검색합니다.

Azure 함수 배포

Azure Functions는 여러 가지 방법으로 배포할 수 있습니다. 자세한 내용은 Azure Functions 배포에 대한 자세한 내용은 Azure Functions 문서배포 섹션을 참조하세요.

TokenProvider 구현

TokenProviders는 여러 가지 방법으로 구현할 수 있지만 두 개의 별도 API 호출인 fetchOrdererTokenfetchStorageToken을 구현해야 합니다. 이러한 API는 각각 Fluid orderer 및 스토리지 서비스에 대한 토큰을 페치합니다. 두 함수 모두 토큰 값을 나타내는 TokenResponse 개체를 반환합니다. Fluid Framework 런타임은 토큰을 검색하기 위해 필요에 따라 이러한 두 API를 호출합니다. 애플리케이션 코드가 하나의 서비스 엔드포인트만 사용하여 Azure Fluid Relay 서비스와의 연결을 설정하는 동안 서비스와 함께 azure-client는 내부적으로 하나의 엔드포인트를 orderer 및 스토리지 엔드포인트 쌍으로 변환합니다. 이러한 두 엔드포인트는 해당 세션에 대해 해당 지점에서 사용되므로 토큰을 가져오기 위해 각각 하나씩 두 개의 별도 함수를 구현해야 합니다.

테넌트 비밀 키가 안전하게 유지되도록 하기 위해 보안 백 엔드 위치에 저장되고 Azure Function 내에서만 액세스할 수 있습니다. 토큰을 검색하려면 배포된 Azure Function에 GET 또는 POST를 요청하여 tenantIDdocumentId, 그리고 userID/userName을 제공해야 합니다. Azure Function은 테넌트 ID와 테넌트 키 비밀 간의 매핑을 담당하여 토큰을 적절하게 생성하고 서명합니다.

아래 예제 구현에서는 Azure Function에 대한 이러한 요청을 처리합니다. 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;
    }
}

효율성 및 오류 처리 추가

사용자 AzureFunctionTokenProvider 고유의 TokenProvider 사용자 지정 토큰 공급자를 구현할 때 시작점으로 처리해야 하는 간단한 구현입니다. 프로덕션 준비 토큰 공급자의 구현을 위해 토큰 공급자가 처리해야 하는 다양한 오류 시나리오를 고려해야 합니다. 예를 들어 구현은 AzureFunctionTokenProvider 클라이언트 쪽에서 토큰을 캐시하지 않으므로 네트워크 연결 끊기 상황을 처리하지 못합니다.

컨테이너 연결이 끊어지면 연결 관리자는 컨테이너에 다시 연결하기 전에 TokenProvider에서 새 토큰을 가져오려고 시도합니다. 네트워크 연결이 끊긴 동안 API 가져오기 요청 fetchOrdererToken 이 실패하고 다시 시도할 수 없는 오류가 throw됩니다. 이로 인해 컨테이너가 삭제되고 네트워크 연결이 다시 설정되더라도 다시 연결할 수 없습니다.

이 연결 끊기 문제의 잠재적 해결 방법은 Window.localStorage에서 유효한 토큰을 캐시하는 것입니다. 토큰 캐싱을 사용하면 네트워크 연결이 끊어지는 동안 API 가져오기 요청을 만드는 대신 컨테이너가 유효한 저장된 토큰을 검색합니다. 로컬로 저장된 토큰은 일정 기간 후에 만료될 수 있으며 유효한 새 토큰을 가져오려면 API 요청을 수행해야 합니다. 이 경우 단일 시도 실패 후 컨테이너가 삭제되지 않도록 하려면 추가 오류 처리 및 재시도 논리가 필요합니다.

이러한 개선 사항을 구현하는 방법은 사용자와 애플리케이션의 요구 사항에 전적으로 달려 있습니다. 토큰 솔루션을 사용하면 localStoragegetContainer 호출에서 네트워크 요청을 제거하므로 애플리케이션의 성능이 향상됩니다.

토큰 localStorage 캐싱은 보안에 영향을 미칠 수 있으며 애플리케이션에 적합한 솔루션을 결정할 때 사용자의 재량에 따라 결정됩니다. 토큰 캐싱을 구현하든 그렇지 않든 간에 오류 처리 및 재시도 논리 fetchOrdererTokenfetchStorageToken 를 추가하여 단일 호출 실패 후 컨테이너가 삭제되지 않도록 해야 합니다. 예를 들어 지정된 재시도 횟수 후에만 오류를 다시 시도하고 throw하는 블록으로 블록 catch 의 호출 getTokentry 을 래핑하는 것이 좋습니다.

참고 항목