如何验证对 .NET 中的 Discord 交互 webhook 发出的请求?
How can you authenticate requests made to a Discord interactions webhook in .NET?
根据 Discord documentation,webhook 必须验证每个请求的 headers 才能被接受。该文档提供了以下代码示例:
const nacl = require('tweetnacl');
// Your public key can be found on your application in the Developer Portal
const PUBLIC_KEY = 'APPLICATION_PUBLIC_KEY';
const signature = req.get('X-Signature-Ed25519');
const timestamp = req.get('X-Signature-Timestamp');
const body = req.rawBody; // rawBody is expected to be a string, not raw bytes
const isVerified = nacl.sign.detached.verify(
Buffer.from(timestamp + body),
Buffer.from(signature, 'hex'),
Buffer.from(PUBLIC_KEY, 'hex')
);
if (!isVerified) {
return res.status(401).end('invalid request signature');
}
如何在 .NET 5.0 中执行此操作?我没能找到任何 Ed25519 验证的例子。
此实现需要 NSec.Cryptography NuGet package.
首先,您必须创建一个 ActionFilter 以放置在您的 WebAPI 控制器或端点上。最简单的方法是扩展 ActionFilterAttribute:
public class DiscordAuthorizationActionFilterAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// This is needed to move the request stream to the beginning as it has already been evaluated for model binding
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
var signature = context.HttpContext.Request.Headers["X-Signature-Ed25519"].FirstOrDefault();
var timestamp = context.HttpContext.Request.Headers["X-Signature-Timestamp"].FirstOrDefault();
var body = await new StreamReader(context.HttpContext.Request.Body).ReadToEndAsync();
var key = "{YOUR API KEY HERE}";
var algorithm = SignatureAlgorithm.Ed25519;
var publicKey = PublicKey.Import(algorithm, GetBytesFromHexString(key), KeyBlobFormat.RawPublicKey);
var data = Encoding.UTF8.GetBytes(timestamp + body);
var verified = algorithm.Verify(publicKey, data, GetBytesFromHexString(signature));
if (!verified)
context.Result = new UnauthorizedObjectResult("Invalid request");
else
await next();
}
private byte[] GetBytesFromHexString(string hex)
{
var length = hex.Length;
var bytes = new byte[length / 2];
for (int i = 0; i < length; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
}
注意这条评论:
// This is needed to move the request stream to the beginning as it has already been evaluated for model binding
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
要允许重用请求主体流,您必须在第一次访问请求管道之前在请求管道中显式启用它,可能是在模型绑定期间。为此,您可以在 Startup.cs 中添加一个简单的步骤:
public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
{
app.UseStaticFiles();
app.UseRouting();
// This is needed to retrieve request body as JSON string in ActionFilter
app.Use(async (context, next) =>
{
var controller = context.Request.RouteValues["controller"] as string;
if (controller == "Discord")
context.Request.EnableBuffering();
await next();
});
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
注意检查控制器名称;因为我将属性放在 DiscordController 上,所以存储在 RouteValues 集合中的控制器值是“Discord”。
最后,只需将属性添加到接受 POST 请求的端点:
public class DiscordController : ControllerBase
{
[DiscordAuthorizationActionFilter]
[HttpPost]
public async Task<IActionResult> PostAsync(DiscordInteraction interaction)
{
if (interaction == null)
return BadRequest();
if (interaction.Type == DiscordInteractionType.Ping)
return Ok(new { Type = 1 });
// Request processing here
return Ok();
}
}
请注意,DiscordInteraction 模型是自定义代码,在我所知道的任何库中都不可用。按照文档创建它很简单。为了测试这一点,我发现使用 ngrok 将请求从 Discord 路由到我的开发环境很有帮助。
根据 Discord documentation,webhook 必须验证每个请求的 headers 才能被接受。该文档提供了以下代码示例:
const nacl = require('tweetnacl');
// Your public key can be found on your application in the Developer Portal
const PUBLIC_KEY = 'APPLICATION_PUBLIC_KEY';
const signature = req.get('X-Signature-Ed25519');
const timestamp = req.get('X-Signature-Timestamp');
const body = req.rawBody; // rawBody is expected to be a string, not raw bytes
const isVerified = nacl.sign.detached.verify(
Buffer.from(timestamp + body),
Buffer.from(signature, 'hex'),
Buffer.from(PUBLIC_KEY, 'hex')
);
if (!isVerified) {
return res.status(401).end('invalid request signature');
}
如何在 .NET 5.0 中执行此操作?我没能找到任何 Ed25519 验证的例子。
此实现需要 NSec.Cryptography NuGet package.
首先,您必须创建一个 ActionFilter 以放置在您的 WebAPI 控制器或端点上。最简单的方法是扩展 ActionFilterAttribute:
public class DiscordAuthorizationActionFilterAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// This is needed to move the request stream to the beginning as it has already been evaluated for model binding
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
var signature = context.HttpContext.Request.Headers["X-Signature-Ed25519"].FirstOrDefault();
var timestamp = context.HttpContext.Request.Headers["X-Signature-Timestamp"].FirstOrDefault();
var body = await new StreamReader(context.HttpContext.Request.Body).ReadToEndAsync();
var key = "{YOUR API KEY HERE}";
var algorithm = SignatureAlgorithm.Ed25519;
var publicKey = PublicKey.Import(algorithm, GetBytesFromHexString(key), KeyBlobFormat.RawPublicKey);
var data = Encoding.UTF8.GetBytes(timestamp + body);
var verified = algorithm.Verify(publicKey, data, GetBytesFromHexString(signature));
if (!verified)
context.Result = new UnauthorizedObjectResult("Invalid request");
else
await next();
}
private byte[] GetBytesFromHexString(string hex)
{
var length = hex.Length;
var bytes = new byte[length / 2];
for (int i = 0; i < length; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
}
注意这条评论:
// This is needed to move the request stream to the beginning as it has already been evaluated for model binding
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
要允许重用请求主体流,您必须在第一次访问请求管道之前在请求管道中显式启用它,可能是在模型绑定期间。为此,您可以在 Startup.cs 中添加一个简单的步骤:
public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
{
app.UseStaticFiles();
app.UseRouting();
// This is needed to retrieve request body as JSON string in ActionFilter
app.Use(async (context, next) =>
{
var controller = context.Request.RouteValues["controller"] as string;
if (controller == "Discord")
context.Request.EnableBuffering();
await next();
});
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
注意检查控制器名称;因为我将属性放在 DiscordController 上,所以存储在 RouteValues 集合中的控制器值是“Discord”。
最后,只需将属性添加到接受 POST 请求的端点:
public class DiscordController : ControllerBase
{
[DiscordAuthorizationActionFilter]
[HttpPost]
public async Task<IActionResult> PostAsync(DiscordInteraction interaction)
{
if (interaction == null)
return BadRequest();
if (interaction.Type == DiscordInteractionType.Ping)
return Ok(new { Type = 1 });
// Request processing here
return Ok();
}
}
请注意,DiscordInteraction 模型是自定义代码,在我所知道的任何库中都不可用。按照文档创建它很简单。为了测试这一点,我发现使用 ngrok 将请求从 Discord 路由到我的开发环境很有帮助。