Interazione umana in Funzioni permanenti - Esempio di verifica telefonica

Questo esempio illustra come creare un'orchestrazione di Funzioni permanenti che prevede interazione umana. Ogni volta che una persona reale è coinvolta in un processo automatizzato, il processo deve essere in grado di inviare notifiche alla persona e di ricevere risposte in modo asincrono. È inoltre necessario consentire la possibilità che la persona non sia disponibile. In questa parte i timeout diventano importanti.

L'esempio implementa un sistema di verifica telefonica basata su SMS. Questi tipi di flussi vengono spesso usati quando si verifica il numero di telefono di un cliente o per l'autenticazione a più fattori. L'esempio è particolarmente efficace perché l'intera implementazione viene eseguita solo tramite un paio di brevi funzioni. Non è necessario alcun archivio dati esterno, ad esempio un database.

Nota

La versione 4 del modello di programmazione Node.js per Funzioni di Azure è disponibile a livello generale. Il nuovo modello v4 è progettato per offrire un'esperienza più flessibile e intuitiva agli sviluppatori JavaScript e TypeScript. Altre informazioni sulle differenze tra v3 e v4 sono disponibili nella guida alla migrazione.

Nei frammenti di codice seguenti JavaScript (PM4) indica il modello di programmazione V4, la nuova esperienza.

Prerequisiti

Panoramica dello scenario

La verifica telefonica viene usata per assicurarsi che gli utenti finali dell'applicazione non siano spammer e che siano effettivamente chi affermano di essere. L'autenticazione a più fattori è un metodo di uso comune per proteggere gli account utente da pirati informatici. Il problema nell'implementazione di una verifica telefonica consiste nella necessità di un'l'interazione con stato con una persona fisica. A un utente finale viene in genere inviato un codice, ad esempio un numero di 4 cifre, e l'utente deve rispondere in un intervallo di tempo ragionevole.

Funzioni di Azure è un servizio normalmente senza stato (come molti altri endpoint cloud su altre piattaforme), quindi questi tipi di interazioni comportano la gestione esplicita di uno stato esternamente, ad esempio in un database o in un altro archivio permanente. L'interazione deve essere anche suddivisa in più funzioni che possono essere coordinate tra loro. È necessario ad esempio disporre almeno di una funzione per la scelta di un codice, la permanenza in un punto e l'invio al telefono dell'utente. È necessaria anche almeno un'altra funzione per ricevere una risposta da parte dell'utente e associarla alla chiamata di funzione originale al fine di convalidare il codice. Un timeout è un aspetto importante per garantire la protezione. Lo scenario può diventare complesso rapidamente.

La complessità dello scenario viene notevolmente ridotta grazie all'uso di Funzioni permanenti. Come si vedrà in questo esempio, una funzione dell'agente di orchestrazione può gestire l'interazione con stato in modo semplice e senza coinvolgere alcun archivio dati esterno. Poiché le funzioni dell'agente di orchestrazione sono permanenti, questi flussi interattivi sono anche estremamente affidabili.

Configurazione dell'integrazione di Twilio

Questo esempio prevede l'uso del servizio Twilio per inviare messaggi SMS al telefono cellulare. Funzioni di Azure supporta già Twilio tramite l'associazione a Twilio e l'esempio usa tale funzionalità.

È necessario per prima cosa disporre di un account Twilio. È possibile crearne uno gratuitamente all'indirizzo https://www.twilio.com/try-twilio. Dopo aver creato un account, aggiungere le tre impostazioni all'app per le funzioni.

Nome impostazione app Descrizione valore
TwilioAccountSid SID dell'account Twilio
TwilioAuthToken Token di autenticazione per l'account Twilio
TwilioPhoneNumber Numero di telefono associato all'account Twilio usato per inviare messaggi SMS.

Funzioni

L'articolo illustra le funzioni seguenti nell'app di esempio:

Nota

La funzione HttpStart nell'app di esempio e nella guida introduttiva funge da client di orchestrazione che attiva la funzione dell'agente di orchestrazione.

Funzione dell’agente di orchestrazione E4_SmsPhoneVerification

[FunctionName("E4_SmsPhoneVerification")]
public static async Task<bool> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    string phoneNumber = context.GetInput<string>();
    if (string.IsNullOrEmpty(phoneNumber))
    {
        throw new ArgumentNullException(
            nameof(phoneNumber),
            "A phone number input is required.");
    }

    int challengeCode = await context.CallActivityAsync<int>(
        "E4_SendSmsChallenge",
        phoneNumber);

    using (var timeoutCts = new CancellationTokenSource())
    {
        // The user has 90 seconds to respond with the code they received in the SMS message.
        DateTime expiration = context.CurrentUtcDateTime.AddSeconds(90);
        Task timeoutTask = context.CreateTimer(expiration, timeoutCts.Token);

        bool authorized = false;
        for (int retryCount = 0; retryCount <= 3; retryCount++)
        {
            Task<int> challengeResponseTask =
                context.WaitForExternalEvent<int>("SmsChallengeResponse");

            Task winner = await Task.WhenAny(challengeResponseTask, timeoutTask);
            if (winner == challengeResponseTask)
            {
                // We got back a response! Compare it to the challenge code.
                if (challengeResponseTask.Result == challengeCode)
                {
                    authorized = true;
                    break;
                }
            }
            else
            {
                // Timeout expired
                break;
            }
        }

        if (!timeoutTask.IsCompleted)
        {
            // All pending timers must be complete or canceled before the function exits.
            timeoutCts.Cancel();
        }

        return authorized;
    }
}

Nota

Benché non sia immediatamente ovvio, questo agente di orchestrazione non viola il vincolo di orchestrazione deterministico. È deterministico perché la proprietà CurrentUtcDateTime viene usata per calcolare l'ora di scadenza del timer e restituisce lo stesso valore a ogni riesecuzione in questo punto del codice dell'agente di orchestrazione. Questo comportamento è importante per garantire gli stessi risultati winner da ogni chiamata ripetuta a Task.WhenAny.

Dopo l'avvio, le operazioni di questa funzione dell'agente di orchestrazione sono le seguenti:

  1. Acquisizione di un numero di telefono a cui inviare la notifica SMS.
  2. Chiamata a E4_SendSmsChallenge per inviare un messaggio SMS all'utente e restituzione del codice di autenticazione di 4 cifre previsto.
  3. Creazione di un timer permanente che attivi un intervallo di 90 secondi a partire dal momento corrente.
  4. In parallelo con il timer, attesa di un evento SmsChallengeResponse da parte dell'utente.

L'utente riceve un messaggio SMS con un codice di quattro cifre Hanno 90 secondi di tempo per inviare lo stesso codice di quattro cifre all'istanza della funzione dell'agente di orchestrazione per completare il processo di verifica. Se invia il codice non corretto, l'utente ha a disposizione altri tre tentativi (all'interno dello stesso intervallo di 90 secondi).

Avviso

È importante annullare i timer se non è più necessario che scadano, come illustrato nell'esempio precedente, quando viene accettata una risposta alla richiesta.

Funzione dell’attività E4_SendSmsChallenge

La funzione E4_SendSmsChallenge usa l'associazione a Twilio per inviare il messaggio SMS con codice di 4 cifre all'utente finale.

[FunctionName("E4_SendSmsChallenge")]
public static int SendSmsChallenge(
    [ActivityTrigger] string phoneNumber,
    ILogger log,
    [TwilioSms(AccountSidSetting = "TwilioAccountSid", AuthTokenSetting = "TwilioAuthToken", From = "%TwilioPhoneNumber%")]
        out CreateMessageOptions message)
{
    // Get a random number generator with a random seed (not time-based)
    var rand = new Random(Guid.NewGuid().GetHashCode());
    int challengeCode = rand.Next(10000);

    log.LogInformation($"Sending verification code {challengeCode} to {phoneNumber}.");

    message = new CreateMessageOptions(new PhoneNumber(phoneNumber));
    message.Body = $"Your verification code is {challengeCode:0000}";

    return challengeCode;
}

Nota

È prima necessario installare il pacchetto Nuget Microsoft.Azure.WebJobs.Extensions.Twilio per Funzioni per eseguire il codice di esempio. Non installare anche il pacchetto NuGet Twilio principale perché ciò può causare problemi di controllo delle versioni con conseguenti errori di compilazione.

Eseguire l'esempio

Con le funzioni attivate da HTTP incluse nell'esempio, è possibile avviare l'orchestrazione inviando la richiesta HTTP POST seguente:

POST http://{host}/orchestrators/E4_SmsPhoneVerification
Content-Length: 14
Content-Type: application/json

"+1425XXXXXXX"
HTTP/1.1 202 Accepted
Content-Length: 695
Content-Type: application/json; charset=utf-8
Location: http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}

{"id":"741c65651d4c40cea29acdd5bb47baf1","statusQueryGetUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}","sendEventPostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}","terminatePostUri":"http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/terminate?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}"}

La funzione dell'agente di orchestrazione riceve il numero di telefono fornito e invia immediatamente un messaggio SMS con un codice di verifica di 4 cifre generato casualmente, ad esempio 2168. La funzione attende quindi 90 secondi per ricevere una risposta.

Per rispondere con il codice, è possibile usare RaiseEventAsync (.NET) o raiseEvent (JavaScript/TypeScript) in un'altra funzione o richiamare il webhook HTTP POST sendEventPostUri a cui si è fatto riferimento nella risposta 202 precedente, sostituendo {eventName} con il nome dell'evento, SmsChallengeResponse:

POST http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1/raiseEvent/SmsChallengeResponse?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
Content-Length: 4
Content-Type: application/json

2168

Se si esegue l'invio prima della scadenza del timer, l'orchestrazione viene completato e il campo output viene impostato su true, che indica un esito positivo della verifica.

GET http://{host}/runtime/webhooks/durabletask/instances/741c65651d4c40cea29acdd5bb47baf1?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
HTTP/1.1 200 OK
Content-Length: 144
Content-Type: application/json; charset=utf-8

{"runtimeStatus":"Completed","input":"+1425XXXXXXX","output":true,"createdTime":"2017-06-29T19:10:49Z","lastUpdatedTime":"2017-06-29T19:12:23Z"}

Se si lascia che il timer scada o se si immette il codice errato quattro volte, è possibile eseguire una query sullo stato e visualizzare un output false della funzione di orchestrazione, che indica che la verifica telefonica non è riuscita.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 145

{"runtimeStatus":"Completed","input":"+1425XXXXXXX","output":false,"createdTime":"2017-06-29T19:20:49Z","lastUpdatedTime":"2017-06-29T19:22:23Z"}

Passaggi successivi

In questo esempio sono state illustrate alcune delle funzionalità avanzate di Durable Functions, in particolare le API WaitForExternalEvent e CreateTimer. È stato illustrato come queste funzionalità possono essere combinate con Task.WaitAny (C#)/context.df.Task.any (JavaScript/TypeScript)/context.task_any (Python) per implementare un sistema di timeout affidabile, spesso utile per l’interazione con utenti reali. Per altre informazioni su come usare Funzioni permanenti, fare riferimento alla serie di articoli in cui sono trattati in dettaglio argomenti specifici.