Interação humana no Durable Functions – Exemplo de verificação do telemóvel

Este exemplo demonstra como criar uma orquestração Durable Functions que envolve interação humana. Sempre que uma pessoa real estiver envolvida num processo automatizado, o processo tem de ser capaz de enviar notificações à pessoa e receber respostas de forma assíncrona. Também tem de permitir a possibilidade de a pessoa estar indisponível. (Esta última parte é onde os tempos limite se tornam importantes.)

Este exemplo implementa um sistema de verificação por telefone baseado em SMS. Estes tipos de fluxos são frequentemente utilizados ao verificar o número de telefone de um cliente ou para a autenticação multifator (MFA). É um exemplo avançado porque toda a implementação é feita com algumas funções pequenas. Não é necessário nenhum arquivo de dados externo, como uma base de dados.

Nota

A versão 4 do modelo de programação Node.js para Funções do Azure está geralmente disponível. O novo modelo v4 foi concebido para ter uma experiência mais flexível e intuitiva para programadores de JavaScript e TypeScript. Saiba mais sobre as diferenças entre v3 e v4 no guia de migração.

Nos fragmentos de código seguintes, o JavaScript (PM4) indica o modelo de programação V4, a nova experiência.

Pré-requisitos

Scenario overview (Descrição geral do cenário)

A verificação do telemóvel é utilizada para verificar se os utilizadores finais da sua aplicação não são spammers e que são quem dizem ser. A autenticação multifator é um caso de utilização comum para proteger contas de utilizador de hackers. O desafio de implementar a verificação do seu próprio telemóvel é que requer uma interação com estado com um ser humano. Normalmente, é fornecido algum código a um utilizador final (por exemplo, um número de 4 dígitos) e tem de responder num período de tempo razoável.

As Funções do Azure comuns não têm monitorização de estado (tal como muitos outros pontos finais da cloud noutras plataformas), pelo que estes tipos de interações envolvem gerir explicitamente o estado externamente numa base de dados ou noutro arquivo persistente. Além disso, a interação tem de ser dividida em múltiplas funções que podem ser coordenadas em conjunto. Por exemplo, precisa de, pelo menos, uma função para decidir um código, mantê-lo algures e enviá-lo para o telemóvel do utilizador. Além disso, precisa de, pelo menos, uma outra função para receber uma resposta do utilizador e, de alguma forma, mapeá-la de volta para a chamada da função original para fazer a validação do código. Um tempo limite também é um aspeto importante para garantir a segurança. Pode ficar bastante complexo rapidamente.

A complexidade deste cenário é significativamente reduzida quando utiliza Durable Functions. Como verá neste exemplo, uma função de orquestrador pode gerir a interação com monitorização de estado facilmente e sem envolver quaisquer arquivos de dados externos. Como as funções do orquestrador são duráveis, estes fluxos interativos também são altamente fiáveis.

Configurar a integração do Twilio

Este exemplo envolve a utilização do serviço Twilio para enviar mensagens SMS para um telemóvel. Funções do Azure já tem suporte para o Twilio através do enlace do Twilio e o exemplo utiliza essa funcionalidade.

A primeira coisa de que precisa é de uma conta do Twilio. Pode criar uma gratuitamente em https://www.twilio.com/try-twilio. Assim que tiver uma conta, adicione as seguintes três definições de aplicação à sua aplicação de funções.

Nome da definição da aplicação Descrição do valor
TwilioAccountSid O SID da sua conta do Twilio
TwilioAuthToken O token de Autenticação para a sua conta do Twilio
TwilioPhoneNumber O número de telefone associado à sua conta do Twilio. Isto é utilizado para enviar mensagens SMS.

As funções

Este artigo explica as seguintes funções na aplicação de exemplo:

  • E4_SmsPhoneVerification: uma função de orquestrador que executa o processo de verificação do telemóvel, incluindo a gestão de tempos limite e repetições.
  • E4_SendSmsChallenge: uma função de atividade que envia um código por mensagem de texto.

Nota

A HttpStart função na aplicação de exemplo e o início rápido atuam como cliente Orchestration , o que aciona a função do orquestrador.

E4_SmsPhoneVerification função orchestrator

[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

Pode não ser óbvio no início, mas este orquestrador não viola a restrição de orquestração determinista. É determinista porque a CurrentUtcDateTime propriedade é utilizada para calcular o tempo de expiração do temporizador e devolve o mesmo valor em cada repetição neste momento no código do orquestrador. Este comportamento é importante para garantir que os mesmos winner resultados de cada chamada repetida para Task.WhenAny.

Uma vez iniciada, esta função de orquestrador faz o seguinte:

  1. Obtém um número de telefone para o qual enviará a notificação por SMS.
  2. Chama E4_SendSmsChallenge para enviar uma mensagem SMS ao utilizador e devolve o código de desafio de 4 dígitos esperado.
  3. Cria um temporizador durável que aciona 90 segundos a partir da hora atual.
  4. Em paralelo com o temporizador, aguarda um evento SmsChallengeResponse do utilizador.

O utilizador recebe uma mensagem SMS com um código de quatro dígitos. Têm 90 segundos para enviar esse mesmo código de quatro dígitos de volta para a instância da função do orquestrador para concluir o processo de verificação. Se submeterem o código errado, obterão três tentativas adicionais para o corrigir (na mesma janela de 90 segundos).

Aviso

É importante cancelar os temporizadores se já não precisar que expirem, como no exemplo acima, quando uma resposta de desafio é aceite.

E4_SendSmsChallenge função de atividade

A função E4_SendSmsChallenge utiliza o enlace do Twilio para enviar a mensagem SMS com o código de quatro dígitos ao utilizador final.

[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

Primeiro, tem de instalar o Microsoft.Azure.WebJobs.Extensions.Twilio pacote Nuget para as Funções executarem o código de exemplo. Não instale também o pacote nuget principal do Twilio , porque isto pode causar problemas de controlo de versões que resultam em erros de compilação.

Executar o exemplo

Ao utilizar as funções acionadas por HTTP incluídas no exemplo, pode iniciar a orquestração ao enviar o seguinte pedido 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}"}

A função orchestrator recebe o número de telefone fornecido e envia-lhe imediatamente uma mensagem SMS com um código de verificação de 4 dígitos gerado aleatoriamente, por exemplo, 2168. Em seguida, a função aguarda 90 segundos para obter uma resposta.

Para responder com o código, pode utilizar RaiseEventAsync (.NET) ou raiseEvent (JavaScript/TypeScript) dentro de outra função ou invocar o webhook sendEventPostUri HTTP POST referenciado na resposta 202 acima, substituindo {eventName} pelo nome do 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 enviar isto antes de o temporizador expirar, a orquestração é concluída e o output campo está definido como true, o que indica uma verificação bem-sucedida.

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 deixar o temporizador expirar ou se introduzir o código errado quatro vezes, pode consultar o estado e ver uma false saída da função orchestration, indicando que a verificação do telemóvel falhou.

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

Passos seguintes

Este exemplo demonstrou algumas das capacidades avançadas de Durable Functions, nomeadamente WaitForExternalEvent e CreateTimer APIs. Viu como estes podem ser combinados com Task.WaitAny (C#)/context.df.Task.any (JavaScript/TypeScript)/context.task_any (Python) para implementar um sistema de tempo limite fiável, o que é muitas vezes útil para interagir com pessoas reais. Pode saber mais sobre como utilizar Durable Functions ao ler uma série de artigos que oferecem uma cobertura aprofundada de tópicos específicos.