你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

Durable Functions 中的人机交互 - 电话验证示例

此示例演示如何生成涉及人机交互的 Durable Functions 业务流程。 每当自动化过程中需要真人时,该过程需要能够以异步方式向人员发送通知和接收响应。 它还需要考虑该人员没有时间的可能。 (最后这一部分,超时变得很重要。)

这个示例实现了基于短信的电话验证系统。 验证客户的电话号码或进行多重身份验证 (MFA) 时,经常使用这些类型的流。 这是一个功能强大的例子,因为整个实现是采用几个小型函数完成的。 无需外部数据存储(如数据库)。

注意

适用于 Azure Functions 的 Node.js 编程模型版本 4 现已正式发布。 新版 v4 模型旨在为 JavaScript 和 TypeScript 开发人员提供更为灵活和直观的体验。 在迁移指南中详细了解 v3 和 v4 之间的差异。

在以下代码片段中,JavaScript (PM4) 表示编程模型 V4,即新体验。

先决条件

方案概述

电话验证用于验证应用程序的最终用户不是垃圾邮件发送者,且他们提供的是真实身份。 多重身份验证是保护用户帐户免受黑客攻击的常见用例。 实现自己的电话验证需要面临的挑战是,它需要与人进行有状态交互。 最终用户通常会获得一些代码(例如一个 4 位数字),且必须在合理的时间内响应

普通的 Azure Functions 是无状态的(正如其他平台上的许多其他云终结点一样),因此这些类型的交互需要在外部的数据库或某种其他永久性存储中显式管理状态。 此外,需要将交互分解为多个可一起进行协调的函数。 例如,需要至少一个函数用于确定代码、将其持久保存在某个位置并发送至用户的电话。 此外,还需要至少一个其他函数来接收用户的响应,并以某种方式映射回原始函数调用,以实现代码验证。 超时也是保证安全的一个重要方面。 这可能很快就会变得相当复杂。

如果使用 Durable Functions,可大大降低此方案的复杂性。 如此示例中所示,业务流程协调程序函数可以轻松地管理有状态交互,且无需任何外部数据存储。 由于业务流程协调程序函数是持久的,因此这些交互流也非常可靠

配置 Twilio 集成

此示例涉及使用 Twilio 服务向移动电话发送短信。 Azure Functions 已通过 Twilio 绑定提供对 Twilio 的支持,此示例使用了这一功能。

首先,需要一个 Twilio 帐户。 可以通过 https://www.twilio.com/try-twilio 免费创建一个帐户。 创建帐户以后,向函数应用添加以下三个应用设置

应用设置名称 值说明
TwilioAccountSid Twilio 帐户的 SID
TwilioAuthToken Twilio 帐户的身份验证令牌
TwilioPhoneNumber 与 Twilio 帐户关联的电话号码。 此电话号码用于发送短信。

函数

本文通过示例应用介绍了以下函数:

注意

示例应用和快速入门中的 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 属性用于计算计时器到期时间,且此属性在每次在业务流程协调程序代码中的此时点进行重播均返回相同的值。 为确保对 winner 的每次重复调用均产生相同的 Task.WhenAny 结果,这一行为很重要。

启动后,该业务流程协调程序函数执行以下任务:

  1. 获取要向其发送短信通知的电话号码
  2. 调用 E4_SendSmsChallenge,向用户发送短信,并返回预期的 4 位数质询代码
  3. 创建可从当前时间开始触发 90 秒的持久计时器。
  4. 与计时器一起,等待来自用户的 SmsChallengeResponse 事件

用户会收到一条含 4 位数代码的短信。 用户需要在 90 秒内将相同的 4 位数代码发送回业务流程协调程序函数实例,以便完成验证过程。 如果提交的代码不正确,可额外尝试 3 次进行更正(在相同的 90 秒时间段内)。

警告

如果不再需要计时器到期,请务必取消计时器,正如在上面的示例中收到质询响应后一样。

E4_SendSmsChallenge 活动函数

E4_SendSmsChallenge 函数使用 Twilio 绑定向最终用户发送包含 4 位数代码的短信。

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

注意

必须先安装用于 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}"}

业务流程协调程序函数可接收提供的电话号码,并立即向其发送短信,其中包含随机生成的 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"}

如果计时器到期,或者 4 次输入错误的代码,可查询状态并看到业务流程函数输出为 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) 结合,实现可靠的超时系统,这通常对与真人进行交互非常有用。 可以通过阅读一系列深入讨论了特定主题的文章来了解有关如何使用 Durable Functions 的详细信息。