Condividi tramite


Procedura: Scrivere un tokenProvider con una funzione di Azure

Nota

Questa versione di anteprima viene fornita senza contratto di servizio e non è consigliata per carichi di lavoro di produzione. Alcune funzionalità potrebbero non essere supportate o potrebbero presentare funzionalità limitate.

In Fluid Framework, TokenProviders sono responsabili della creazione e della firma di token usati @fluidframework/azure-client da per effettuare richieste al servizio Inoltro fluido di Azure. Fluid Framework fornisce un TokenProvider semplice e non sicuro a scopo di sviluppo, denominato in modo appropriato InsecureTokenProvider. Ogni servizio Fluid deve implementare un tokenProvider personalizzato in base alle considerazioni di sicurezza e autenticazione del servizio specifico.

A ogni risorsa di Inoltro fluido di Azure creata viene assegnato un ID tenant e la propria chiave privata del tenant univoca. La chiave privata è un segreto condiviso. L'app o il servizio lo conosce e il servizio Inoltro fluido di Azure lo conosce. TokenProviders deve conoscere la chiave privata per firmare le richieste, ma la chiave privata non può essere inclusa nel codice client.

Implementare una funzione di Azure per firmare i token

Un'opzione per la creazione di un provider di token sicuro consiste nel creare un endpoint HTTPS e creare un'implementazione tokenProvider che effettua richieste HTTPS autenticate a tale endpoint per recuperare i token. Questo percorso consente di archiviare la chiave privata del tenant in una posizione sicura, ad esempio Azure Key Vault.

La soluzione completa include due componenti:

  1. Endpoint HTTPS che accetta le richieste e restituisce token di inoltro fluido di Azure.
  2. Implementazione di ITokenProvider che accetta un URL a un endpoint, quindi effettua richieste a tale endpoint per recuperare i token.

Creare un endpoint per TokenProvider usando Funzioni di Azure

L'uso di Funzioni di Azure è un modo rapido per creare un endpoint HTTPS di questo tipo.

Questo esempio illustra come creare una funzione di Azure HTTPTrigger personalizzata che recupera il token passando la chiave del tenant.

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;

La generateToken funzione, trovata nel @fluidframework/azure-service-utils pacchetto, genera un token per l'utente specificato firmato usando la chiave privata del tenant. Questo metodo consente di restituire il token al client senza esporre il segreto. Il token viene invece generato sul lato server usando il segreto per fornire l'accesso con ambito al documento specificato. L'esempio ITokenProvider seguente invia richieste HTTP a questa funzione di Azure per recuperare i token.

Distribuire la funzione di Azure

Funzioni di Azure possono essere distribuiti in diversi modi. Per altre informazioni, vedere la sezione Distribuzione della documentazione di Funzioni di Azure per altre informazioni sulla distribuzione di Funzioni di Azure.

Implementare TokenProvider

I tokenProvider possono essere implementati in molti modi, ma devono implementare due chiamate API separate: fetchOrdererToken e fetchStorageToken. Queste API sono responsabili del recupero dei token rispettivamente per i servizi di ordinamento e archiviazione Fluid. Entrambe le funzioni restituiscono TokenResponse oggetti che rappresentano il valore del token. Il runtime di Fluid Framework chiama queste due API in base alle esigenze per recuperare i token. Si noti che mentre il codice dell'applicazione usa un solo endpoint di servizio per stabilire la connettività con il servizio Inoltro fluido di Azure, il client azure internamente insieme al servizio converte tale endpoint in una coppia di endpoint di ordinamento e archiviazione. Questi due endpoint vengono usati da quel punto per quella sessione, motivo per cui è necessario implementare le due funzioni separate per il recupero di token, uno per ognuno.

Per garantire che la chiave privata del tenant sia protetta, viene archiviata in una posizione back-end sicura ed è accessibile solo dall'interno della funzione di Azure. Per recuperare i token, è necessario effettuare una GET richiesta o POST alla funzione di Azure distribuita, specificando e documentIdtenantID e userID/userName. La funzione di Azure è responsabile del mapping tra l'ID tenant e un segreto della chiave del tenant per generare e firmare il token in modo appropriato.

L'implementazione di esempio seguente gestisce l'esecuzione di queste richieste alla funzione di Azure. Usa la libreria axios per effettuare richieste HTTP. È possibile usare altre librerie o approcci per effettuare una richiesta HTTP dal codice del server. Questa implementazione specifica viene fornita anche come esportazione dal @fluidframework/azure-client pacchetto.

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

Aggiungere efficienza e gestione degli errori

AzureFunctionTokenProvider è una semplice implementazione di TokenProvider che deve essere considerata come punto di partenza quando si implementa il proprio provider di token personalizzato. Per l'implementazione di un provider di token pronto per la produzione, è consigliabile considerare diversi scenari di errore che il provider di token deve gestire. Ad esempio, l'implementazione AzureFunctionTokenProvider non riesce a gestire le situazioni di disconnessione di rete perché non memorizza nella cache il token sul lato client.

Quando il contenitore si disconnette, la gestione connessione tenta di ottenere un nuovo token da TokenProvider prima di riconnettersi al contenitore. Mentre la rete è disconnessa, la richiesta di recupero dell'API eseguita in fetchOrdererToken avrà esito negativo e genererà un errore non ritentabile. A sua volta, il contenitore viene eliminato e non è in grado di riconnettersi anche se viene ristabilita una connessione di rete.

Una possibile soluzione per questo problema di disconnessione consiste nel memorizzare nella cache i token validi in Window.localStorage. Con la memorizzazione nella cache dei token il contenitore recupererà un token archiviato valido invece di effettuare una richiesta di recupero dell'API mentre la rete è disconnessa. Si noti che un token archiviato in locale potrebbe scadere dopo un determinato periodo di tempo ed è comunque necessario effettuare una richiesta API per ottenere un nuovo token valido. In questo caso, sarebbe necessaria una gestione aggiuntiva degli errori e una logica di ripetizione dei tentativi per impedire al contenitore di eliminare dopo un singolo tentativo non riuscito.

Il modo in cui si sceglie di implementare questi miglioramenti spetta completamente all'utente e ai requisiti dell'applicazione. Si noti che con la localStorage soluzione token verranno visualizzati anche miglioramenti delle prestazioni nell'applicazione perché si rimuove una richiesta di rete in ogni getContainer chiamata.

La memorizzazione nella cache dei token con qualcosa di simile localStorage può comportare implicazioni per la sicurezza e spetta alla propria discrezione quando si decide quale soluzione è appropriata per l'applicazione. Indipendentemente dal fatto che si implementi o meno la memorizzazione nella cache dei token, è necessario aggiungere la logica di gestione degli errori e riprovare in fetchOrdererToken e fetchStorageToken in modo che il contenitore non venga eliminato dopo una singola chiamata non riuscita. Si consideri, ad esempio, il wrapping della chiamata di getToken in un try blocco con un catch blocco che ritenta e genera un errore solo dopo un numero specificato di tentativi.

Vedi anche