長期函式中的人為互動 - 電話驗證範例

這個範例會示範如何建置長期函式協調流程,其中牽涉到人為互動。 每當在自動化程序中牽涉到真人,處理程序必須能夠傳送通知給人員,並以非同步方式接收回應。 它也必須允許人員沒有空的可能性。 (此最後一個部分就是逾時變得重要的地方。)

這個範例會實作以 SMS 為基礎的電話驗證系統。 這些類型的流量通常會在驗證客戶的電話號碼時,或針對多重要素驗證 (MFA) 使用。 這是功能強大的範例,因為整個實作是使用幾個小型函式來完成。 不需要外部資料存放區,例如資料庫。

注意

適用於 Azure Functions 的第 4 版 Node.js 程式設計模型已正式推出。 新的 v4 模型旨在為 JavaScript 和 TypeScript 開發人員提供更靈活的直覺式體驗。 如需深入了解 v3 與 v4 之間的差異,請參閱移轉指南

在下列程式碼片段中,JavaScript (PM4) 表示程式設計模型 V4,這是新的體驗。

必要條件

案例概觀

電話驗證是用來驗證您應用程式的使用者不是濫發垃圾郵件者,而且確實是他們所聲稱的人。 多重要素驗證是保護使用者帳戶免於駭客入侵的常見使用案例。 實作您自己的電話驗證的挑戰,是它需要與真人的可設定狀態互動。 終端使用者通常會提供一些代碼 (例如,4 位數數字),並且必須在合理的時間量之內回應。

一般而言,Azure Functions 是無狀態 (如同其他平台上的其他許多雲端端點),所以這些互動類型會明確牽涉到在外部的資料庫或其他某些永續性存放區中管理狀態。 此外,互動必須細分成可以協調在一起的多個函式。 例如,您需要至少一個函式以決定程式碼、保存在某處,以及將它傳送到使用者的電話。 而且您還需要至少一個其他的函式,以接收來自使用者的回應,並且以某種方式將其對應回原始函式呼叫,以進行程式碼驗證。 逾時也是確保安全性的重要層面。 很快就會變得相當複雜。

當您使用長期函式時,此案例的複雜性就會大幅減少。 如同您在這個範例所見,協調器函式可以輕鬆地管理可設定狀態互動,不需要涉及任何外部資料存放區。 因為協調器函式是「長期」的,所以這些互動流量也非常可靠。

設定 Twilio 整合

這個範例牽涉到使用 Twilio 服務來將 SMS 訊息傳送至行動電話。 Azure Functions 已透過 Twilio 繫結支援 Twilio,範例會使用該功能。

首先您需要的是 Twilio 帳戶。 您可以在 https://www.twilio.com/try-twilio 免費建立一個帳戶。 一旦您擁有帳戶,將下列三個應用程式設定新增至您的函式應用程式。

應用程式設定名稱 值描述
TwilioAccountSid Twilio 帳戶的 SID
TwilioAuthToken Twilio 帳戶的驗證權杖
TwilioPhoneNumber 與您的 Twilio 帳戶相關聯的電話號碼。 這是用來傳送 SMS 訊息。

函式

本文會逐步解說範例應用程式中的下列函式:

  • E4_SmsPhoneVerification:執行電話驗證流程的協調器函式,包括管理逾時和重試。
  • E4_SendSmsChallenge:透過簡訊傳送程式碼的活動函式

注意

範例應用程式和快速入門作為協調流程用戶端HttpStart 函式,會觸發協調器函式。

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

注意

一開始可能不明顯,但此協調器不會違反確定性協調流程限制式。 此為確定性,是因為 CurrentUtcDateTime 屬性是用來計算計時器到期時間,且會在協調器程式碼中此點的每個重新執行都傳回相同的值。 對於確保每次重複呼叫 Task.WhenAny 都有相同 winner 結果而言,這個行為非常重要。

一旦啟動,此協調器函式會執行下列作業:

  1. 取得電話號碼,SMS 通知會「傳送」給該號碼。
  2. 呼叫 E4_SendSmsChallenge 以將 SMS 訊息傳送給使用者,並且傳回預期的 4 位數挑戰碼。
  3. 建立長期計時器,它會觸發從目前時間起算 90 秒的時間。
  4. 與計時器同時,等候來自使用者的 SmsChallengeResponse 事件。

使用者會收到具有四位數代碼的 SMS 訊息。 他們有 90 秒的時間,將相同的 4 位數代碼傳送回協調器函式執行個體,以完成驗證程序。 如果提交錯誤的代碼,有額外三次嘗試可以進行修正 (在相同的 90 秒時間內)。

警告

如果您已經在上述範例中接受挑戰回應,且不再需要使用計時器,請務必取消計時器

E4_SendSmsChallenge 活動函式

E4_SendSmsChallenge 函式會使用 Twilio 繫結,將具有 4 位數代碼的 SMS 訊息傳送給使用者。

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

注意

您必須先安裝適用於 Azure Functions 的 Microsoft.Azure.WebJobs.Extensions.Twilio Nuget 套件,才能執行範例程式碼。 但不要安裝主要 Twilio nuget 套件,因為這可能會導致版本設定問題而造成建置錯誤。

執行範例

使用範例中所包含的 HTTP 觸發函式,您可以透過傳送下列 HTTP POST 要求來啟動協調流程:

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

協調器函式會接收提供的電話號碼,並且立即傳送 SMS 訊息給該電話號碼,訊息包含隨機產生的 4 位數驗證碼,例如 2168。 此函式接著會在 90 秒的時間內等候回應。

若要回覆代碼,您可以使用另一個函式內的 RaiseEventAsync (.NET) 或 raiseEvent (JavaScript/TypeScript),或叫用上述 202 回應中參考的 sendEventPostUri HTTP POST Webhook,將 {eventName} 取代為事件的名稱,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

如果您計時器到期之前傳送,協調流程就會完成且 output 欄位會設定為 true,指出驗證成功。

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

如果您讓計時器到期,或者如果您輸入錯誤的代碼四次,您可以查詢狀態,並且看到 false 協調流程函式輸出,指出電話驗證失敗。

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

下一步

此範例已示範一些 Durable Functions 的進階功能,尤其是 WaitForExternalEventCreateTimer API。 您已經看到這些功能如何與 Task.WaitAny (C#)/context.df.Task.any (JavaScript/TypeScript)/context.task_any (Python) 結合,以實作可靠的逾時系統,對於與真人的互動通常相當有用。 藉由閱讀一系列深入探討特定主題的文章,您可以深入了解如何使用長期函式。