几年前,我为 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/