Участие пользователя в устойчивых функциях. Пример проверки номера телефона

В этом примере показано, как создать оркестрацию устойчивых функций, которая включает в себя участие пользователя. Каждый раз, когда человек участвует в автоматизированном процессе, процесс должен отправлять уведомления пользователю и получать ответы асинхронно. Он также должен допускать возможность, что пользователь недоступен. (Именно в этой части время ожидания становится важным.)

В этом примере реализуется система проверки номера телефона на основе SMS. Эти типы потоков часто используются при проверке номера телефона клиента или для многофакторной проверки подлинности. Это очень эффективный пример, так как полная реализация выполняется с помощью нескольких небольших функций. Внешнее хранилище данных, например база данных, не требуется.

Примечание

Общедоступна версия 4 модели программирования Node.js для Функции Azure. Новая модель версии 4 предназначена для более гибкого и интуитивно понятного интерфейса для разработчиков JavaScript и TypeScript. Дополнительные сведения о различиях между версиями 3 и 4 см. в руководстве по миграции.

В следующих фрагментах кода JavaScript (PM4) обозначает новую модель программирования версии 4.

Предварительные требования

Общие сведения о сценарии

Проверка номера телефона используется для подтверждения того, что пользователи вашего приложения не рассылают нежелательную почту и являются теми, за кого себя выдают. Многофакторная проверка подлинности часто применяется для защиты учетных записей пользователей от злоумышленников. Проблема реализации собственной проверки номера телефона состоит в том, что требуется взаимодействие с пользователем с отслеживанием состояния. Пользователю обычно предоставляется определенный код (например, 4-значное число), на который он должен отреагировать в течение приемлемого промежутка времени.

Обычно в решении "Функции Azure" не отслеживается состояние (как и в других облачных конечных точках на других платформах), поэтому эти типы взаимодействия требуют явного управления состоянием во внешней базе данных или другом постоянном хранилище. Кроме того, взаимодействие должно быть разбито на несколько функций, которые можно согласовать вместе. Например, необходима хотя бы одна функция для выбора кода, его сохранения и отправки на телефон пользователя. Кроме того, вам также требуется другая функция, чтобы получить ответ от пользователя и каким-то образом обратно сопоставить его с исходным вызовом функции, чтобы выполнить проверку кода. Время ожидания также является важным аспектом обеспечения безопасности. Ситуация может очень быстро усложниться.

Сложность этого сценария значительно снижается, когда вы используете устойчивые функции. Как будет показано в этом примере, с помощью функции оркестратора можно без проблем управлять взаимодействием с отслеживанием состояния без использования любых внешних хранилищ данных. Так как функции оркестратора устойчивые, эти интерактивные потоки также очень надежные.

Настройка интеграции Twilio

Для отправки SMS-сообщений на мобильный телефон в этом примере используется служба Twilio. В Функциях Azure уже реализована поддержка Twilio через привязку Twilio. В этом образце используется эта функция.

Первое, что вам нужно — это учетная запись Twilio. Вы можете создать бесплатную учетную запись здесь: https://www.twilio.com/try-twilio. Получив учетную запись, добавьте следующие три параметра приложения в приложение-функцию.

Имя параметра приложения Описание значения
TwilioAccountSid Идентификатор безопасности вашей учетной записи в Twilio.
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-сообщение с 4-значным кодом. У них есть 90 секунд, чтобы отправить этот код обратно в экземпляр функции оркестратора для завершения проверки. Если пользователи отправляют неверный код, у них есть еще три попытки (в пределах тех же 90 секунд).

Предупреждение

Если таймеры вам больше не нужны, как показано в примере выше, когда принимается ответ на запрос, отключите их.

Функция действия E4_SendSmsChallenge

Функция E4_SendSmsChallenge использует привязку Twilio для отправки 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;
}

Примечание

Сначала необходимо установить пакет NuGet Microsoft.Azure.WebJobs.Extensions.Twilio для Функций, чтобы запустить пример кода. Не устанавливайте основной пакет NuGet Twilio, так как это может привести к проблемам с управлением версиями, а значит и ошибкам сборки.

Запуск примера

С помощью функций, активируемых по 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) в другой функции или вызвать веб-перехватчик SENDEventPostUri HTTP POST, на который ссылается ответ 202 выше, заменив {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"}

Дальнейшие действия

В этом примере показаны некоторые расширенные возможности устойчивых функций, в частности API WaitForExternalEvent и CreateTimer. Вы узнали, как их можно объединить с Task.WaitAny (C#)/context.df.Task.any (JavaScript/TypeScript)/context.task_any (Python) для реализации надежной системы времени ожидания, которая часто полезна для взаимодействия с реальными людьми. Дополнительные сведения об использовании устойчивых функций см. в серии статей, где подробно рассматриваются определенные темы.