ASP.NET Core 8 Web API中Audit.NET对接Azure表存储的PartitionKey错误
在ASP.NET Core 8 Web API中基于Audit.NET实现审计追踪,使用AzureStorageTableDataProvider存储审计事件时,遇到PartitionKey相关的400错误:
The 'PartitionKey' parameter of value 'GET:https://demoapi.azurewebsites.net/api/product/23' is out of range with RequestId:adb38aea-c002-0061-436d-d408ff000000
Time:2024-07-12T15:08:43.7480899Z
Status: 400 (Bad Request)
ErrorCode: OutOfRangeInput
Content:
{ "odata.error":{"code":"OutOfRangeInput","message":{"lang":"en-US","value":"The 'PartitionKey' parameter of value 'GET:https://demoapi.azurewebsites.net/api/product/23' is out of range.\nRequestId:adb38aea-c002-0061-436d-d408ff000000\nTime:2024-07-12T15:08:43.7480899Z"}}}
原配置代码如下:
public void AuditSetupOutput(IApplicationBuilder app) { var options = new JsonSerializerOptions { WriteIndented = true }; Configuration.Setup() .JsonSystemAdapter(options) .UseAzureTableStorage(config => config .Endpoint(new Uri("https://account.table.core.windows.net"), new DefaultAzureCredential()) .TableName(evt => $"{_TargetTableName}{DateTime.UtcNow:MMMyyyy}") .ClientOptions(new TableClientOptions { Retry = { MaxRetries = 3 } }) .EntityBuilder(builder => builder .PartitionKey(auditEvent => auditEvent.EventType) .RowKey(auditEvent => Guid.NewGuid().ToString("N")) .Columns(col => col .FromDictionary(auditEvent => new Dictionary<string, object> { { "EventType", auditEvent.EventType }, { "UserName", auditEvent.Environment.UserName }, { "EventDuration", auditEvent.Duration }, { "DataSize", auditEvent.ToJson().Length }, { "Data", auditEvent.ToJson().Length >= 32000 ? CompressAuditEventData(auditEvent.ToJson()) : auditEvent.ToJson() } }))); var httpContextAccessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>(); Configuration.AddCustomAction(ActionType.OnScopeCreated, scope => { scope.SetCustomField("TraceId", httpContextAccessor.HttpContext?.TraceIdentifier); }); }
原因分析
Azure表存储对PartitionKey有严格限制:
- 最大长度为1024字节(UTF-8编码)
- 不能包含斜杠(
/)、反斜杠(\)、#、?等特殊字符 - 不能以空格结尾
Audit.NET的WebApi审计事件默认EventType为HTTP方法:完整请求URL(如GET:https://demoapi.azurewebsites.net/api/product/23),该值包含禁用的斜杠字符,且如果URL过长会直接超出1024字节限制,触发OutOfRangeInput错误。
解决方法
需要修改PartitionKey的生成逻辑,满足Azure表存储的限制:
- 缩短长度:使用更简洁的标识(如HTTP方法+控制器/动作名称)
- 哈希处理:对原EventType进行哈希运算,生成固定长度的合法字符串
- 清理特殊字符:移除或替换PartitionKey中的禁用字符
参考实现代码
方案1:使用HTTP方法+控制器/动作名称
从AuditWebApi事件中提取控制器和动作名称,生成简洁合规的PartitionKey:
public void AuditSetupOutput(IApplicationBuilder app) { var options = new JsonSerializerOptions { WriteIndented = true }; Configuration.Setup() .JsonSystemAdapter(options) .UseAzureTableStorage(config => config .Endpoint(new Uri("https://account.table.core.windows.net"), new DefaultAzureCredential()) .TableName(evt => $"{_TargetTableName}{DateTime.UtcNow:MMMyyyy}") .ClientOptions(new TableClientOptions { Retry = { MaxRetries = 3 } }) .EntityBuilder(builder => builder // 修改PartitionKey生成逻辑:提取HTTP方法+控制器/动作名称 .PartitionKey(auditEvent => { var webApiAction = auditEvent.GetWebApiAuditAction(); return $"{webApiAction.HttpMethod}:{webApiAction.ControllerName}/{webApiAction.ActionName}"; }) .RowKey(auditEvent => Guid.NewGuid().ToString("N")) .Columns(col => col .FromDictionary(auditEvent => new Dictionary<string, object> { { "EventType", auditEvent.EventType }, { "UserName", auditEvent.Environment.UserName }, { "EventDuration", auditEvent.Duration }, { "DataSize", auditEvent.ToJson().Length }, { "Data", auditEvent.ToJson().Length >= 32000 ? CompressAuditEventData(auditEvent.ToJson()) : auditEvent.ToJson() } }))); var httpContextAccessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>(); Configuration.AddCustomAction(ActionType.OnScopeCreated, scope => { scope.SetCustomField("TraceId", httpContextAccessor.HttpContext?.TraceIdentifier); }); }
方案2:哈希处理EventType
对原EventType进行SHA256哈希,生成固定长度且无特殊字符的PartitionKey:
using System.Security.Cryptography; using System.Text; public void AuditSetupOutput(IApplicationBuilder app) { var options = new JsonSerializerOptions { WriteIndented = true }; Configuration.Setup() .JsonSystemAdapter(options) .UseAzureTableStorage(config => config .Endpoint(new Uri("https://account.table.core.windows.net"), new DefaultAzureCredential()) .TableName(evt => $"{_TargetTableName}{DateTime.UtcNow:MMMyyyy}") .ClientOptions(new TableClientOptions { Retry = { MaxRetries = 3 } }) .EntityBuilder(builder => builder // 修改PartitionKey为EventType的哈希值,替换Azure禁用的字符 .PartitionKey(auditEvent => { using var sha256 = SHA256.Create(); var eventTypeBytes = Encoding.UTF8.GetBytes(auditEvent.EventType); var hashBytes = sha256.ComputeHash(eventTypeBytes); // 转换为Base64并替换Azure禁用的字符 return Convert.ToBase64String(hashBytes) .Replace("/", "_") .Replace("+", "-") .TrimEnd('='); }) .RowKey(auditEvent => Guid.NewGuid().ToString("N")) .Columns(col => col .FromDictionary(auditEvent => new Dictionary<string, object> { { "EventType", auditEvent.EventType }, { "UserName", auditEvent.Environment.UserName }, { "EventDuration", auditEvent.Duration }, { "DataSize", auditEvent.ToJson().Length }, { "Data", auditEvent.ToJson().Length >= 32000 ? CompressAuditEventData(auditEvent.ToJson()) : auditEvent.ToJson() } }))); var httpContextAccessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>(); Configuration.AddCustomAction(ActionType.OnScopeCreated, scope => { scope.SetCustomField("TraceId", httpContextAccessor.HttpContext?.TraceIdentifier); }); }
内容的提问来源于stack exchange,提问作者santosh kumar patro

