.net - 如何将 Alexa 与 Azure Functions v4 结合使用

标签 .net azure azure-functions .net-6.0 alexa-skills-kit

几年前,我为 Alexa 技能创建了一个 POC。此技能使用使用 .NET Core 2.1 编写的 Azure 函数 (v2)。该函数当时返回一个 IActionResult,其中包含 SkillResponse(使用 Alexa.NET 包 (v1.5.7) 生成)。现在我想部署 POC,但我注意到 Azure 函数 < v4 不再受支持,并且我无法再在 Azure 中创建它们。为了解决此问题,我已将 .NET Core 2.1 应用程序更新到 .NET 6。但结果是该函数的响应从 IActionResult 更改为 HttpResponseData,并且 Alexa 技能不再起作用。当我从 Postman 调用该函数时,响应似乎是正确的,但 Alexa 仍然不断返回错误,例如:

空 SpeechletResponse | 请求标识符:speechletResponse 不能为 null

有什么办法可以解决这个问题吗?

该函数看起来像这样:

[Function("Alexa")]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req)
response = ResponseBuilder.Tell("Some text");
response.Response.ShouldEndSession = true;
await httpResponse.WriteAsJsonAsync(response);
return httpResponse;

当我在 postman 中调用此方法时,我得到了正确的响应:

{
    "Version": "1.0",
    "SessionAttributes": null,
    "Response": {
        "OutputSpeech": {
            "Type": "PlainText",
            "Text": "Some text"
        },
        "Card": null,
        "Reprompt": null,
        "ShouldEndSession": true,
        "Directives": []
    }
}

我正在使用 Conveyor 公开我的本地应用程序,但这在 .NET 升级之前不是问题。

欢迎任何帮助。

更新

完整功能代码:

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Alexa.NET;
using Alexa.NET.Request;
using Alexa.NET.Request.Type;
using Alexa.NET.Response;
using Alexa_AF.Models;
using Alexa_AF.RestClient;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;

namespace Alexa_AF
{
    public class AlexaFunction
    {
        private readonly ILogger _logger;

        public AlexaFunction(ILogger<AlexaFunction> logger)
        {
            _logger = logger;
        }

        [Function("Alexa")]
        public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req)
        {
            SkillResponse response = null;
            var httpResponse = req.CreateResponse(HttpStatusCode.OK);

            var json = await req.ReadAsStringAsync();
            var skillRequest = JsonConvert.DeserializeObject<SkillRequest>(json);

            var verified = await Validator.VerifyRequestIsFromAlexa(skillRequest, req);

            if (!verified)
            {
                response = ResponseBuilder.Tell("Something went wrong validating the request");
                response.Response.ShouldEndSession = true;
                await httpResponse.WriteAsJsonAsync(response);
                return httpResponse;
            }

            var accessToken = skillRequest.Context.System.User.AccessToken;
            var user = await VerifyToken(accessToken);

            if (user == null)
            {
                await httpResponse.WriteAsJsonAsync(ResponseBuilder.Tell("Please link your account"));
                return httpResponse;
            }
            var session = skillRequest.Session;


            if (session.Attributes == null) session.Attributes = new Dictionary<string, object>();

            SystemExceptionRequestHandler(skillRequest);

            if (skillRequest.Request is LaunchRequest)
            {
                response = ResponseBuilder.Tell("Welcome");
                response.Response.ShouldEndSession = false;
                await httpResponse.WriteAsJsonAsync(response);
                return httpResponse;
            }


            if (skillRequest.Request is SessionEndedRequest)
            {
                response = ResponseBuilder.Tell("Good bye.");
                response.Response.ShouldEndSession = true;
                await httpResponse.WriteAsJsonAsync(response);
                return httpResponse;
            }


            if (skillRequest.Request is IntentRequest)
            {
                var request = skillRequest.Request as IntentRequest;
                var requestName = request.Intent.Name;

                var t = Type.GetType($"Alexa_AF.IntentClasses.{(requestName.Contains("AMAZON") ? requestName.Split('.')[1] : requestName)}, Alexa-AF");
                var responseMethod = t.GetMethod("Response", BindingFlags.Instance | BindingFlags.Public);
                var classInstance = Activator.CreateInstance(t, null);

                var returnObject = await (Task<SkillResponse>)responseMethod.Invoke(classInstance, new object[] { request, user, session, accessToken });
                await httpResponse.WriteAsJsonAsync(returnObject);
                return httpResponse;
            }

            return httpResponse;
        }

        private static void SystemExceptionRequestHandler(SkillRequest skillRequest)
        {
            if (skillRequest.Request.GetType() != typeof(SystemExceptionRequest)) return;

            var sysException = skillRequest.Request as SystemExceptionRequest;
            var message = sysException.Error.Message;
            var reqID = sysException.ErrorCause.requestId;
            switch (sysException.Error.Type)
            {
                case ErrorType.InvalidResponse:
                    break;
                case ErrorType.DeviceCommunicationError:
                    break;
                case ErrorType.InternalError:
                    break;
                case ErrorType.MediaErrorUnknown:
                    break;
                case ErrorType.InvalidMediaRequest:
                    break;
                case ErrorType.MediaServiceUnavailable:
                    break;
                case ErrorType.InternalServerError:
                    break;
                case ErrorType.InternalDeviceError:
                    break;
            }
        }

        private static async Task<UserModel> VerifyToken(string token)
        {
            if (token == null)
            {
                return null;
            }

            var identityServerUrl = "...";

            if (string.IsNullOrEmpty(identityServerUrl))
            {
                throw new Exception("...");
            }

            var httpClient = new HttpClient();
            var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{identityServerUrl}/...");
            requestMessage.Headers.Add("Authorization", "Bearer " + token);
            var result = await httpClient.SendAsync(requestMessage);
            var resultAsString = await result.Content.ReadAsStringAsync();

            return JsonConvert.DeserializeObject<UserModel>(resultAsString);
        }

    }
}

验证器

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Alexa.NET.Request;
using Microsoft.Azure.Functions.Worker.Http;

namespace Alexa_AF
{
    public static class Validator
    {
        private static readonly string CertHeader = "-----BEGIN CERTIFICATE-----";
        private static readonly string CertFooter = "-----END CERTIFICATE-----";

        private static readonly HttpClient _client = new HttpClient();

        public static async Task<bool> VerifyRequestIsFromAlexa(SkillRequest requestBody, HttpRequestData httpRequest)
        {
            var validatedCertificateChains = new Dictionary<string, X509Certificate2>();

            if (requestBody?.Request?.Timestamp == null)
            {
                throw new InvalidOperationException("Alexa Request Invalid: Request Timestamp Missing");
            }

            var ts = requestBody.Request.Timestamp;
            var tsDiff = (DateTimeOffset.UtcNow - ts).TotalSeconds;

            if (Math.Abs(tsDiff) >= 150)
            {
                throw new InvalidOperationException("Alexa Request Invalid: Request Timestamp outside valid range");
            }

            var certUrls = httpRequest.Headers.GetValues("SignatureCertChainUrl");
            var signatures = httpRequest.Headers.GetValues("Signature");

            var certChainUrl = certUrls.FirstOrDefault();
            var signature = signatures.FirstOrDefault();

            if (string.IsNullOrEmpty(certChainUrl))
            {
                return false;
            }

            if (string.IsNullOrEmpty(signature))
            {
                return false;
            }

            var uri = new Uri(certChainUrl);

            if (uri.Scheme.ToLower() != "https")
            {
                return false;
            }

            if (uri.Port != 443)
            {
                return false;
            }

            if (uri.Host.ToLower() != "s3.amazonaws.com")
            {
                return false;
            }

            if (!uri.AbsolutePath.StartsWith("/echo.api/"))
            {
                return false;
            }

            X509Certificate2 signingCertificate;

            if (!validatedCertificateChains.ContainsKey(uri.ToString()))
            {
                Trace.WriteLine("Validating cert URL: " + certChainUrl);

                var certList = await DownloadPemCertificatesAsync(uri.ToString());

                if (certList == null || certList.Length < 2)
                {
                    return false;
                }

                var primaryCert = certList[0];
                var data = new AsnEncodedData(primaryCert.Extensions[2].Oid, primaryCert.Extensions[2].RawData);
                var text = data.Format(true);

                if (!text.Contains("echo-api.amazon.com"))
                {
                    return false;
                }

                var chainCerts = new List<X509Certificate2>();

                for (var i = 1; i < certList.Length; i++)
                {
                    chainCerts.Add(certList[i]);
                }

                if (!ValidateCertificateChain(primaryCert, chainCerts))
                {
                    return false;
                }


                signingCertificate = primaryCert;

                lock (validatedCertificateChains)
                {
                    if (!validatedCertificateChains.ContainsKey(uri.ToString()))
                    {
                        Trace.WriteLine("Adding validated cert url: " + uri);
                        validatedCertificateChains[uri.ToString()] = primaryCert;
                    }
                    else
                    {
                        Trace.WriteLine("Race condition hit while adding validated cert url: " + uri);
                    }
                }
            }
            else
            {
                signingCertificate = validatedCertificateChains[uri.ToString()];
            }

            if (signingCertificate == null)
            {
                return false;
            }

            var signatureBytes = Convert.FromBase64String(signature);

            var thing = signingCertificate.GetRSAPublicKey();
            return thing?.VerifyData(httpRequest.Body, signatureBytes, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1) ?? false;
        }

        public static bool ValidateCertificateChain(X509Certificate2 certificate, IEnumerable<X509Certificate2> chain)
        {
            using var verifier = new X509Chain();
            verifier.ChainPolicy.ExtraStore.AddRange(chain.ToArray());
            var result = verifier.Build(certificate);
            return result;
        }

        public static X509Certificate2 ParseCertificate(string base64CertificateText)
        {
            var bytes = Convert.FromBase64String(base64CertificateText);
            var cert = new X509Certificate2(bytes);
            return cert;
        }

        public static async Task<X509Certificate2[]> DownloadPemCertificatesAsync(string pemUri)
        {
            var pemText = await _client.GetStringAsync(pemUri);
            return string.IsNullOrEmpty(pemText) ? null : ReadPemCertificates(pemText);
        }


        public static X509Certificate2[] ReadPemCertificates(string pemString)
        {
            var lines = pemString.Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries);
            var certList = new List<string>();
            StringBuilder grouper = null;
            for (var i = 0; i < lines.Length; i++)
            {
                var curLine = lines[i];
                if (curLine.Equals(CertHeader, StringComparison.Ordinal))
                {
                    grouper = new StringBuilder();
                }
                else if (curLine.Equals(CertFooter, StringComparison.Ordinal))
                {
                    certList.Add(grouper.ToString());
                    grouper = null;
                }
                else
                {
                    if (grouper != null) grouper.Append(curLine);
                }
            }

            var collection = new List<X509Certificate2>();

            foreach (var certText in certList)
            {
                var cert = ParseCertificate(certText);
                collection.Add(cert);
            }

            return collection.ToArray();
        }
    }
}

套餐

<PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <RootNamespace>Alexa_AF</RootNamespace>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enabled</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Alexa.NET" Version="1.5.7" />
    <PackageReference Include="Microsoft.ApplicationInsights" Version="2.21.0" />
    <PackageReference Include="TimeZoneConverter" Version="2.5.1" />
    <PackageReference Include="Twilio.AspNet.Core" Version="5.33.1" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.14.1" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.19.0" />
  </ItemGroup>

最佳答案

我发现了这个问题。当我将 .NET Core 2.1 应用程序与更新的 .NET 6 应用程序的结果进行比较时,我注意到 JSON 响应属性过去在 .NET Core 应用程序中以小写字母开头,而现在它们在 .NET Core 应用程序中以大写字母开头.NET 6 应用程序。

当我向 WriteAsJsonAsync 方法添加 JSON 序列化器时,一切又恢复正常了。

var serializerSettings = new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore,
    ContractResolver = new CamelCasePropertyNamesContractResolver()
};

var serializer = new NewtonsoftJsonObjectSerializer(serializerSettings);

await httpResponse.WriteAsJsonAsync(response, serializer);

关于.net - 如何将 Alexa 与 Azure Functions v4 结合使用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76977734/

相关文章:

azure - Azure Functions 上的 VCRedist 2015+

c# - 为什么我的 Azure Function App(根据数据生成 PDF 文件)会超时?

c# - 通过正则表达式验证文件类型

azure - 在 Azure SignalR 中配置到 hubContext.Clients.User 的声明映射

c# - 使用 WMI 调用解决 "Access Denied"异常

azure - Windows Azure 平台就绪测试工具

azure - 套接字异常 : An attempt was made to access a socket in a way forbidden by its access permissions

由于版本不匹配,Azure Function 应用程序无法进行 GitHub 集成

.net - Oracle.ManagedDataAccess 到 AWS RDS 数据库 - TCPS : Invalid SSL Wallet (Magic)

c# - 在 RIA 服务客户端代码生成中排除服务