ASP.NET Core 6自定义Basic认证Post请求Postman报错排查
问题排查:ASP.NET Core 6 Web API POST请求400错误
问题背景
开发的ASP.NET Core 6 Web API采用自定义Basic认证(基于X-UserId请求头),并通过HMAC哈希校验POST请求体。GET请求可正常运行,但调用POST接口时返回400验证错误,提示输入无有效JSON令牌、user字段必填。取消X-Digest头并注释认证处理器中POST相关校验代码后,请求恢复正常。
错误详情
{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-e3ed0f946eea3d3837b9fb1ab1a90264-c22b6030e59a8653-00", "errors": { "$": [ "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0." ], "user": [ "The user field is required." ] } }
相关代码
认证处理器代码
using System.Net.Http.Headers; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; namespace test25_08.Authentication; public class BasicAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions> { private readonly ApplicationDbContext _context; public BasicAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, ApplicationDbContext context) : base(options, logger, encoder, clock) { _context = context; } protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { string method = Request.Method; if (!Request.Headers.ContainsKey("X-UserId")) { return AuthenticateResult.Fail("No header found"); } var headerValue = AuthenticationHeaderValue.Parse(Request.Headers["X-UserId"]); var bytes = Convert.FromBase64String(headerValue.Parameter); var credentials = Encoding.UTF8.GetString(bytes); if (!string.IsNullOrEmpty(credentials)) { var strings = credentials.Split(":"); var userId = strings[0]; var password = strings[1]; var user = _context.Users .FirstOrDefault(item => item.Id == Convert.ToInt32(userId) && item.Password == password ); if (user == null) { return AuthenticateResult.Fail("No user found"); } if (method.Equals("POST")) { string secret = "secret"; var headerValue2 = AuthenticationHeaderValue.Parse(Request.Headers["X-Digest"]); if (!Request.Headers.ContainsKey("X-Digest")) { return AuthenticateResult.Fail("No header found"); } Request.EnableBuffering(); Request.Body.Position = 0; string requestBody = await new StreamReader(Request.Body).ReadToEndAsync(); string replace = Regex.Replace(requestBody, @"\s+", ""); if (!headerValue2.Parameter!.Equals(HashString(replace, secret))) { return AuthenticateResult.Fail("Request body doesn't match"); } } var claim = new[] { new Claim(ClaimTypes.Name, userId) }; var identity = new ClaimsIdentity(claim, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return AuthenticateResult.Success(ticket); } return AuthenticateResult.Fail("UnAuthorized"); } static string HashString(string stringToHash, string hachKey) { UTF8Encoding myEncoder = new UTF8Encoding(); byte[] key = myEncoder.GetBytes(hachKey); byte[] text = myEncoder.GetBytes(stringToHash); HMACSHA1 myHmacsha1 = new HMACSHA1(key); byte[] hashCode = myHmacsha1.ComputeHash(text); string hash = BitConverter.ToString(hashCode).Replace("-", ""); return hash.ToLower(); } }
Program.cs配置代码
using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; using test25_08; using test25_08.Authentication; using test25_08.Service; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); // builder.Services.AddIdentity<User, IdentityRole>() // .AddEntityFrameworkStores<ApplicationDbContext>() // .AddDefaultTokenProviders(); builder.Services.AddScoped<IWalletService, WalletService>(); builder.Services .AddAuthentication("BasicAuthHandler").AddScheme<AuthenticationSchemeOptions,BasicAuthHandler>("BasicAuthHandler", null); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();
curl请求示例
curl --location --request POST 'https://localhost:44358/api/v1/Users' \ --header 'X-UserId: basic MTphYWE=' \ --header 'X-Digest: basic 707cc304a905d573cd196e2ace1eb565e3c04a82' \ --header 'Content-Type: application/json' \ --data-raw '{ "id": 0, "userName": "string", "password": "string", "fullName": "string", "passportNumber": "string", "borNDate": "2022-09-07T03:14:00.985Z", "isAuthenticated": true }'
问题原因及修复方案
核心问题
认证处理器读取POST请求体后,未将Request.Body的位置重置回起始位置,导致后续ModelBinder读取请求体时,流已处于末尾,无法获取JSON内容,从而触发400错误。此外,X-Digest头的校验顺序存在逻辑问题,先解析再检查是否存在,可能引发空引用异常。
修复步骤
重置请求体流位置:读取完请求体后,将
Request.Body.Position重置为0,确保后续组件能正常读取:string requestBody = await new StreamReader(Request.Body).ReadToEndAsync(); string replace = Regex.Replace(requestBody, @"\s+", ""); // 重置流位置 Request.Body.Position = 0;调整X-Digest头校验顺序:先检查头是否存在,再进行解析,避免空引用:
if (method.Equals("POST")) { string secret = "secret"; // 先检查X-Digest头是否存在 if (!Request.Headers.ContainsKey("X-Digest")) { return AuthenticateResult.Fail("No header found"); } // 再解析头内容 var headerValue2 = AuthenticationHeaderValue.Parse(Request.Headers["X-Digest"]); Request.EnableBuffering(); Request.Body.Position = 0; string requestBody = await new StreamReader(Request.Body).ReadToEndAsync(); string replace = Regex.Replace(requestBody, @"\s+", ""); // 重置流位置 Request.Body.Position = 0; if (!headerValue2.Parameter!.Equals(HashString(replace, secret))) { return AuthenticateResult.Fail("Request body doesn't match"); } }校验哈希一致性:确保客户端生成HMAC哈希时,对请求体的处理逻辑(如去除所有空白字符)与服务端完全一致,否则会导致哈希校验不通过。
内容的提问来源于stack exchange,提问作者Nuriddin
相关产品推荐
相关产品推荐

