net接口请求参数可能会被拦截--巨坑
<h2 id="中间件引起的接口请求参数被拦截导致参数一直是null这问题困扰了我很久值得记录">中间件引起的接口请求参数被拦截,导致参数一直是null,这问题困扰了我很久,值得记录</h2><h2 id="1场景">1.场景</h2>
<h3 id="11-客户端使用framework48做一个接口请求发送">1.1 客户端使用framework4.8做一个接口请求发送:</h3>
<pre><code>public static class ApiHelper
{
private static string Internal_ApiUrl = string.Empty;
private static string Client_ApiUrl = string.Empty;
static ApiHelper()
{
Internal_ApiUrl = ConfigurationManager.AppSettings["Internal_ApiUrl"];
Client_ApiUrl = ConfigurationManager.AppSettings["Client_ApiUrl"];
}
public static string GetLicenseUrl()
{
return Internal_ApiUrl + "/Api/License/GetLicense";
}
public static WebApiCallBack GetLicense(string enterpriseName, string uniqueCode,bool IsExistLicense)
{
FMLicense fMLicense = new FMLicense { enterpriseName = enterpriseName, uniqueCode = uniqueCode, isExistLicense = IsExistLicense };
var jsonBody = JsonConvert.SerializeObject(fMLicense, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
return RequestSend(GetLicenseUrl(), "POST", jsonBody);
}
public static WebApiCallBack RequestSend(string serviceUrl, string method, string bodyJson)
{
ServicePointManager.Expect100Continue = false;
var handler = new HttpClientHandler();
using (var client = new HttpClient(handler))
{
Console.WriteLine(bodyJson);
var content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = client.PostAsync(serviceUrl, content).Result;
string result = response.Content.ReadAsStringAsync().Result;
Console.WriteLine(result);
return JsonConvert.DeserializeObject<WebApiCallBack>(result);
}
}
}
</code></pre>
<h3 id="12-服务端">1.2 服务端</h3>
<pre><code>
/")]
public class LicenseController : ControllerBase
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILicenseService _licenseService;
public LicenseController(ILicenseService licenseService, IHttpContextAccessor httpContextAccessor)
{
this._httpContextAccessor = httpContextAccessor;
this._licenseService = licenseService;
}
public async Task<WebApiCallBack> GetLicense(FMLicense License)
{
FMLicense fMLicense = License;
var result = new WebApiCallBack();
if (fMLicense == null)
{
result.code = GlobalStatusCodes.Status400BadRequest;
result.msg = "实体参数为空";
return result;
}
else
{
#region # 验证
if (string.IsNullOrEmpty(fMLicense.enterpriseName))
{
result.code = GlobalStatusCodes.Status400BadRequest;
result.msg = "实体参数为空";
result.otherData = fMLicense;
return result;
}
if (string.IsNullOrEmpty(fMLicense.uniqueCode))
{
result.code = GlobalStatusCodes.Status400BadRequest;
result.msg = "机器唯一码不可为空!";
result.otherData = fMLicense;
return result;
}
#endregion
//业务逻辑
return result;
}
}
</code></pre>
<h3 id="13-写了一个中间件requresplogmildd-记录请求和返回数据的日志">1.3 写了一个中间件RequRespLogMildd ,记录请求和返回数据的日志</h3>
<pre><code>public class RequRespLogMildd
{
private readonly RequestDelegate _next;
public RequRespLogMildd(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (AppSettingsConstVars.MiddlewareRequestResponseLogEnabled)
{
// 过滤,只有接口
if (context.Request.Path.Value.Contains("api") || context.Request.Path.Value.Contains("Api"))
{
//context.Request.EnableBuffering();
Stream originalBody = context.Response.Body;
try
{
// 存储请求数据
await RequestDataLog(context);
using (var ms = new MemoryStream())
{
context.Response.Body = ms;
await _next(context);
// 存储响应数据
ResponseDataLog(context.Response, ms);
ms.Position = 0;
await ms.CopyToAsync(originalBody);
}
}
catch (Exception ex)
{
// 记录异常
//ErrorLogData(context.Response, ex);
Parallel.For(0, 1, e =>
{
LogLockHelper.OutErrorLog("ErrorLog", "ErrorLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new string[] { "Request Data:", ex.Message, ex.StackTrace });
});
}
finally
{
context.Response.Body = originalBody;
}
}
else
{
await _next(context);
}
}
else
{
await _next(context);
}
}
private async Task RequestDataLog(HttpContext context)
{
var request = context.Request;
var sr = new StreamReader(request.Body);
var content = $" QueryData:{request.Path + request.QueryString}\r\n BodyData:{await sr.ReadToEndAsync()}";
if (!string.IsNullOrEmpty(content))
{
Parallel.For(0, 1, e =>
{
LogLockHelper.OutSql2Log("RequestResponseLog", "RequestResponseLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new string[] { "Request Data:", content });
});
//request.Body.Position = 0;
}
}
private void ResponseDataLog(HttpResponse response, MemoryStream ms)
{
ms.Position = 0;
var ResponseBody = new StreamReader(ms).ReadToEnd();
// 去除 Html
var reg = "<[^>]+>";
var isHtml = Regex.IsMatch(ResponseBody, reg);
if (!string.IsNullOrEmpty(ResponseBody))
{
Parallel.For(0, 1, e =>
{
LogLockHelper.OutSql2Log("RequestResponseLog", "RequestResponseLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new string[] { "Response Data:", ResponseBody });
});
}
}
}
</code></pre>
<p>以上中间件,在后端Program类中使用 app.UseRequestResponseLog();</p>
<p>不管使用客户端/postman/apifox 调用接口GetLicense时都会报错,请求的json格式一直错误,错误信息如下</p>
<pre><code>{
"errors": {
"": [
"A non-empty request body is required."
],
"license": [
"The License field is required."
]
},
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-deaf4252040738321b26fc1fd3718696-ea7ecf0fa2bb0491-00"
}
</code></pre>
<h3 id="14-错误原因是-web-api-pipeline-被修改">1.4 错误原因是 Web API pipeline 被修改</h3>
<p>某些中间件可能拦截请求流(body),例如使用了某些日志中间件或反复读取 body 的 filter,可能会导致模型绑定失败。检查 Startup.cs 或 Program.cs 中是否有读取 Request.Body 的地方。</p>
<p><strong>ASP.NET Core 的模型绑定器只能读取一次 HttpRequest.Body。你在 RequestDataLog() 中读取了 Body,但没有重置流的位置:</strong></p>
<pre><code>var sr = new StreamReader(request.Body);
var content = $"... {await sr.ReadToEndAsync()}";
</code></pre>
<p>之后没有重置 request.Body.Position = 0;,所以模型绑定器读到的是空流。</p>
<h2 id="2-解决方案">2 解决方案</h2>
<p>要 <strong>读取并保留请求体内容供后续使用</strong>,你需要:</p>
<ol>
<li>启用请求体缓冲:context.Request.EnableBuffering();</li>
<li>读取后重置流的位置:request.Body.Position = 0;</li>
</ol>
<p><strong>优化后的代码:</strong></p>
<pre><code>public class RequRespLogMildd
{
private readonly RequestDelegate _next;
public RequRespLogMildd(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (AppSettingsConstVars.MiddlewareRequestResponseLogEnabled)
{
if (context.Request.Path.Value.Contains("api", StringComparison.OrdinalIgnoreCase))
{
Stream originalBody = context.Response.Body;
try
{
// ✅ 启用请求体缓冲
context.Request.EnableBuffering();
// 存储请求数据
await RequestDataLog(context);
using (var ms = new MemoryStream())
{
context.Response.Body = ms;
await _next(context);
// 存储响应数据
ResponseDataLog(context.Response, ms);
ms.Position = 0;
await ms.CopyToAsync(originalBody);
}
}
catch (Exception ex)
{
Parallel.For(0, 1, e =>
{
LogLockHelper.OutErrorLog("ErrorLog", "ErrorLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new[] { "Request Data:", ex.Message, ex.StackTrace });
});
}
finally
{
context.Response.Body = originalBody;
}
}
else
{
await _next(context);
}
}
else
{
await _next(context);
}
}
private async Task RequestDataLog(HttpContext context)
{
var request = context.Request;
// ✅ 读取前先设置 Position = 0
request.Body.Position = 0;
//// leaveOpen: true 确保读取后流还可以被使用
//是否根据字节顺序标记(BOM)来检测编码:true(默认)检测 BOM,如果发现 BOM,则用它指定的编码代替传入的 Encoding.UTF8;false 不检测 BOM,严格使用你传入的 Encoding.UTF8。
using var reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
var body = await reader.ReadToEndAsync();
// ✅ 读取完之后重置位置供后续使用
request.Body.Position = 0;
var content = $" QueryData:{request.Path + request.QueryString}\r\n BodyData:{body}";
if (!string.IsNullOrEmpty(content))
{
Parallel.For(0, 1, e =>
{
LogLockHelper.OutSql2Log("RequestResponseLog", "RequestResponseLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new[] { "Request Data:", content });
});
}
}
private void ResponseDataLog(HttpResponse response, MemoryStream ms)
{
ms.Position = 0;
var ResponseBody = new StreamReader(ms).ReadToEnd();
var reg = "<[^>]+>";
var isHtml = Regex.IsMatch(ResponseBody, reg);
if (!string.IsNullOrEmpty(ResponseBody))
{
Parallel.For(0, 1, e =>
{
LogLockHelper.OutSql2Log("RequestResponseLog", "RequestResponseLog" + DateTime.Now.ToString("yyyy-MM-dd-HH"), new[] { "Response Data:", ResponseBody });
});
}
}
}
</code></pre>
<p><strong>如果本文介绍对你有帮助,可以一键四连:点赞+评论+收藏+推荐,谢谢!</strong></p><br><br>
来源:https://www.cnblogs.com/chenshibao/p/18901936 LZ分析得很到位!这个坑确实很典型,很多人在写请求日志中间件时都会遇到这个问题。
问题的核心就是LZ说的:**ASP.NET Core的模型绑定器只能读取一次Request.Body**。当你用StreamReader读取后,流的Position已经到末尾了,后面模型绑定再读就是空的了。
你的解决方案完全正确,补充几点注意事项:
1. **EnableBuffering()** 最好在中间件最开始、请求刚进入时就调用,放在if判断里面也行,但更稳妥的做法是在最外层就启用缓冲,防止后面某些逻辑需要多次读取。
2. **leaveOpen: true** 这个参数很重要!如果不设置,using dispose的时候会把流关闭,后续模型绑定就彻底凉凉了。
3. 其实还有个小问题,你的中间件只判断了Path包含"api",但如果有些内部接口不带api路径也会中招,建议用更精确的路由匹配或者用特性来判断。
4. 另外LZ可以考虑用**MiddlewareFilter**或者直接用**ActionFilter**来做请求日志,这样可以在模型绑定之后、action执行之后获取到已经绑定好的参数,不用自己手动读body那么麻烦。
不过你的方案已经很成熟了,记录下来对新手很有帮助!赞一个!👍
另外LZ用的Parallel.For来写日志其实有点杀鸡用牛刀了,直接写异步日志库或者用Queue处理就行,不然高并发的时候可能会有些性能问题。
頁:
[1]