C# ASP.NET 不缓冲完整请求体如何读取请求Body前几个字节做验证
首先你之前的自定义拦截流稳定性差的核心原因是:首次ReadAsync返回字节数不足4时会直接标记校验完成,跳过校验逻辑,导致非法文件也可能通过校验。
以下是更稳定的实现方案,全程仅缓冲最多4个字节,完全不需要对整个请求流做磁盘/内存缓冲:
优化后拦截流实现
public class InterceptorStream : Stream { private readonly Stream _innerStream; private readonly byte[] _prefixBuffer = new byte[4]; private int _prefixBytesRead; private bool _isValidated; public InterceptorStream(Stream innerStream) { _innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream)); } public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return await ReadAsync(buffer.AsMemory(offset, count), cancellationToken); } public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) { // 校验已完成直接透传读取 if (_isValidated) { return await _innerStream.ReadAsync(buffer, cancellationToken); } // 未凑够4字节前缀时持续读取 if (_prefixBytesRead < _prefixBuffer.Length) { var remaining = _prefixBuffer.Length - _prefixBytesRead; var readCount = await _innerStream.ReadAsync(_prefixBuffer.AsMemory(_prefixBytesRead, remaining), cancellationToken); if (readCount == 0) { _isValidated = true; throw new InvalidDataException("非法文件:文件长度不足4字节"); } _prefixBytesRead += readCount; // 凑够前缀后执行校验 if (_prefixBytesRead == _prefixBuffer.Length) { _isValidated = true; if (!_prefixBuffer.SequenceEqual(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF })) { throw new InvalidDataException("非法文件:魔术字校验不通过"); } } } // 将已读取的前缀字节复制到返回缓冲区 var copyCount = Math.Min(_prefixBytesRead, buffer.Length); _prefixBuffer.AsSpan(0, copyCount).CopyTo(buffer.Span); _prefixBytesRead -= copyCount; // 剩余未返回的前缀字节移到缓冲区头部 if (_prefixBytesRead > 0) { _prefixBuffer.AsSpan(copyCount, _prefixBytesRead).CopyTo(_prefixBuffer); } // 缓冲区还有剩余空间的话读取更多字节返回 if (copyCount < buffer.Length) { var additionalCount = await _innerStream.ReadAsync(buffer.Slice(copyCount), cancellationToken); copyCount += additionalCount; } return copyCount; } // 其余Stream方法直接透传给内层流即可 public override bool CanRead => _innerStream.CanRead; public override bool CanSeek => _innerStream.CanSeek; public override bool CanWrite => _innerStream.CanWrite; public override long Length => _innerStream.Length; public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; } public override void Flush() => _innerStream.Flush(); public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); public override void SetLength(long value) => _innerStream.SetLength(value); public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count); }
控制器改造
不需要再调用Request.EnableBuffering(),直接用拦截流包装请求体传入SDK即可:
public async Task<IActionResult> Post() { using var interceptedStream = new InterceptorStream(Request.Body); // SDK读取流时会自动触发校验,校验失败会直接抛出异常 await client.ProcessFileAsync(interceptedStream); return StatusCode(201, id); }
方案优势
- 全程仅缓冲最多4个字节,不会将整个请求流写入内存或磁盘,内存占用极低
- 不存在校验跳过的情况:要么凑够4字节完成校验,要么流提前结束直接判定为非法
- 所有读取到的字节都会完整返回给调用方,不会出现数据丢失
- 不需要修改外部SDK和原有校验逻辑,侵入性极低
内容的提问来源于stack exchange,提问作者NotFound
相关产品推荐
相关产品推荐

