Dela via


Mänsklig interaktion i Durable Functions – Exempel på telefonverifiering

Det här exemplet visar hur du skapar en Durable Functions orkestrering som inbegriper mänsklig interaktion. När en verklig person är involverad i en automatiserad process måste processen kunna skicka meddelanden till personen och ta emot svar asynkront. Det måste också möjliggöra möjligheten att personen inte är tillgänglig. (Det är i den sista delen som tidsgränser blir viktiga.)

Det här exemplet implementerar ett SMS-baserat telefonverifieringssystem. Dessa typer av flöden används ofta när du verifierar en kunds telefonnummer eller för multifaktorautentisering (MFA). Det är ett kraftfullt exempel eftersom hela implementeringen görs med några små funktioner. Inget externt datalager, till exempel en databas, krävs.

Anteckning

Version 4 av Node.js programmeringsmodell för Azure Functions är allmänt tillgänglig. Den nya v4-modellen är utformad för att ha en mer flexibel och intuitiv upplevelse för JavaScript- och TypeScript-utvecklare. Läs mer om skillnaderna mellan v3 och v4 i migreringsguiden.

I följande kodfragment anger JavaScript (PM4) programmeringsmodellen V4, den nya upplevelsen.

Förutsättningar

Översikt över scenario

Telefonverifiering används för att verifiera att slutanvändarna av ditt program inte är spammare och att de är de som de säger att de är. Multifaktorautentisering är ett vanligt användningsfall för att skydda användarkonton från hackare. Utmaningen med att implementera din egen telefonverifiering är att det krävs en tillståndskänslig interaktion med en människa. En slutanvändare får vanligtvis kod (till exempel ett fyrsiffrigt nummer) och måste svara inom rimlig tid.

Vanliga Azure Functions är tillståndslösa (liksom många andra molnslutpunkter på andra plattformar), så dessa typer av interaktioner omfattar uttryckligen hantering av tillstånd externt i en databas eller något annat beständigt arkiv. Dessutom måste interaktionen delas upp i flera funktioner som kan samordnas tillsammans. Du behöver till exempel minst en funktion för att bestämma en kod, spara den någonstans och skicka den till användarens telefon. Dessutom behöver du minst en annan funktion för att ta emot ett svar från användaren och på något sätt mappa tillbaka den till det ursprungliga funktionsanropet för att kunna utföra kodvalidering. En timeout är också en viktig aspekt för att garantera säkerheten. Det kan bli ganska komplext snabbt.

Komplexiteten i det här scenariot minskar avsevärt när du använder Durable Functions. Som du ser i det här exemplet kan en orkestreringsfunktion enkelt hantera den tillståndskänsliga interaktionen utan att involvera några externa datalager. Eftersom orkestreringsfunktioner är hållbara är dessa interaktiva flöden också mycket tillförlitliga.

Konfigurera Twilio-integrering

I det här exemplet används Twilio-tjänsten för att skicka SMS till en mobiltelefon. Azure Functions har redan stöd för Twilio via Twilio-bindningen, och exemplet använder den funktionen.

Det första du behöver är ett Twilio-konto. Du kan skapa en kostnadsfri på https://www.twilio.com/try-twilio. När du har ett konto lägger du till följande tre appinställningar i funktionsappen.

Namn på appinställning Värdebeskrivning
TwilioAccountSid SID för ditt Twilio-konto
TwilioAuthToken Autentiseringstoken för ditt Twilio-konto
TwilioPhoneNumber Det telefonnummer som är kopplat till ditt Twilio-konto. Detta används för att skicka SMS.

Funktionerna

Den här artikeln går igenom följande funktioner i exempelappen:

  • E4_SmsPhoneVerification: En orkestreringsfunktion som utför telefonverifieringsprocessen, inklusive hantering av tidsgränser och återförsök.
  • E4_SendSmsChallenge: En aktivitetsfunktion som skickar en kod via sms.

Anteckning

Funktionen HttpStart i exempelappen och snabbstarten fungerar som Orchestration-klient som utlöser orchestrator-funktionen.

E4_SmsPhoneVerification orchestrator-funktion

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

Anteckning

Det kanske inte är uppenbart först, men den här orkestratorn bryter inte mot den deterministiska orkestreringsbegränsningen. Det är deterministiskt eftersom CurrentUtcDateTime egenskapen används för att beräkna timerns förfallotid och returnerar samma värde för varje repetition vid den här tidpunkten i orchestrator-koden. Det här beteendet är viktigt för att säkerställa att samma winner resultat från varje upprepat anrop till Task.WhenAny.

När den här orkestreringsfunktionen har startats gör den följande:

  1. Hämtar ett telefonnummer som sms-meddelandet skickas till.
  2. Anropar E4_SendSmsChallenge för att skicka ett SMS till användaren och returnerar den förväntade fyrsiffriga utmaningskoden.
  3. Skapar en varaktig timer som utlöser 90 sekunder från den aktuella tiden.
  4. Parallellt med timern väntar du på en SmsChallengeResponse-händelse från användaren.

Användaren får ett SMS med en fyrsiffrig kod. De har 90 sekunder på sig att skicka tillbaka samma fyrsiffriga kod till orchestrator-funktionsinstansen för att slutföra verifieringsprocessen. Om de skickar fel kod får de ytterligare tre försök att få rätt (inom samma 90-sekundersfönster).

Varning

Det är viktigt att avbryta timers om du inte längre behöver att de upphör att gälla, som i exemplet ovan när ett utmaningssvar accepteras.

E4_SendSmsChallenge aktivitetsfunktion

Funktionen E4_SendSmsChallenge använder Twilio-bindningen för att skicka SMS-meddelandet med fyrsiffrig kod till slutanvändaren.

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

Anteckning

Du måste först installera Microsoft.Azure.WebJobs.Extensions.Twilio Nuget-paketet för Functions för att köra exempelkoden. Installera inte också det huvudsakliga Twilio-nuget-paketet eftersom detta kan orsaka versionsfel som resulterar i byggfel.

Kör exemplet

Med hjälp av de HTTP-utlösta funktionerna i exemplet kan du starta orkestreringen genom att skicka följande HTTP POST-begäran:

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

Orchestrator-funktionen tar emot det angivna telefonnumret och skickar omedelbart ett SMS med en slumpmässigt genererad 4-siffrig verifieringskod, till exempel 2168. Funktionen väntar sedan 90 sekunder på ett svar.

Om du vill svara med koden kan du använda RaiseEventAsync (.NET) eller raiseEvent (JavaScript/TypeScript) i en annan funktion eller anropa http post-webhooken sendEventPostUri som refereras i 202-svaret ovan, och ersätta {eventName} med namnet på händelsen: 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

Om du skickar detta innan timern upphör att gälla slutförs orkestreringen och output fältet är inställt truepå , vilket indikerar en lyckad verifiering.

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

Om du låter timern förfalla, eller om du anger fel kod fyra gånger, kan du fråga efter statusen och se utdata från en false orkestreringsfunktion, vilket indikerar att telefonverifieringen misslyckades.

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

Nästa steg

Det här exemplet har visat några av de avancerade funktionerna i Durable Functions, särskilt WaitForExternalEvent API:er och CreateTimer . Du har sett hur dessa kan kombineras med Task.WaitAny (C#)/context.df.Task.any (JavaScript/TypeScript)/context.task_any (Python) för att implementera ett tillförlitligt timeout-system, vilket ofta är användbart för att interagera med riktiga människor. Du kan lära dig mer om hur du använder Durable Functions genom att läsa en serie artiklar som ger detaljerad information om specifika ämnen.