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. Si tratta di un esempio potente perché l'intera implementazione viene eseguita usando un paio di piccole 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 agli sviluppatori JavaScript e TypeScript un'esperienza più flessibile e intuitiva. 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. Può diventare abbastanza 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 del 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 HttpStart funzione nell'app di esempio e la 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

Potrebbe non essere ovvio all'inizio, ma questo agente di orchestrazione non viola il vincolo di orchestrazione deterministica. È deterministico perché la CurrentUtcDateTime proprietà viene usata per calcolare la scadenza del timer e restituisce lo stesso valore in ogni riproduzione in questo punto nel codice dell'agente di orchestrazione. Questo comportamento è importante per garantire che gli stessi winner risultati 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 per inviare lo stesso codice a 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 di attività E4_SendSmsChallenge

La funzione E4_SendSmsChallenge usa l'associazione Twilio per inviare il messaggio SMS con il codice a quattro 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 Microsoft.Azure.WebJobs.Extensions.Twilio pacchetto Nuget per Funzioni per eseguire il codice di esempio. Non installare anche il pacchetto nuget Twilio principale perché questo può causare problemi di controllo delle versioni che causano 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 la invia immediatamente un messaggio SMS con un codice di verifica a 4 cifre generato casualmente, ad esempio 2168. La funzione attende quindi 90 secondi per ricevere una risposta.

Per rispondere al codice, è possibile usare RaiseEventAsync (.NET) o raiseEvent (JavaScript/TypeScript) all'interno di un'altra funzione o richiamare il webhook HTTP POST sendEventPostUri a cui fa 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

Questo esempio ha dimostrato alcune delle funzionalità avanzate di Durable Functions, in particolare WaitForExternalEvent e CreateTimer api. Si è visto come questi possono essere combinati con Task.WaitAny (C#)/context.df.Task.any (JavaScript/TypeScript)context.task_any / (Python) per implementare un sistema di timeout affidabile, che spesso è utile per interagire con persone reali. Per altre informazioni su come usare Funzioni permanenti, fare riferimento alla serie di articoli in cui sono trattati in dettaglio argomenti specifici.