當你的服務需要隱藏真實的訊息給調用端時,除了在每一個 Action 各自處理,Middleware 也是選擇之一,本想這應該是很簡單的操作,沒想到碰了一鼻子灰,以下是我複寫 HttpContext.Response.Body 的步驟,希望對正觀看的你有幫助。
開發環境
- Windows 11
- .NET 6
- Rider 2022.2.1
Middleware 生命週期
Middleware 的執行順序是由 Startup 配置的順序決定,順序在上面的 Request 會先執行,順序在下面的 Response 會先執行
如何在 Middleware 複寫 HttpContext.Response.Body
在 Middleware 的 HttpContext.Response.Body (Stream) 是不可讀的,需要把它換成可讀的 MemoryStream,最後處理完畢之後,必須要再換回來
Step1. 換成可以讀取的 MemoryStream,如下圖:
Step2. HttpContext.Response.Body 換回原來的 Stream
這裡做了幾件事
- 讀取 Response.Body 的內容,並寫入到 log
- 替換原本的 Response.Body,把真實的內容換成不明確的 await context.Response.WriteAsync(fuzzyData);
首先,新增一個 .NET 6 的 Library 專案,並安裝以下套件
dotnet add package Microsoft.AspNetCore.Http.Abstractions --version 2.2.0
dotnet add package Microsoft.Extensions.Logging.Abstractions --version 6.0.1
OverrideResponseHandlerMiddleware 完整代碼如下:
public class OverrideResponseHandlerMiddleware
{
private readonly RequestDelegate _next;
public OverrideResponseHandlerMiddleware(RequestDelegate next)
{
this._next = next;
}
public async Task InvokeAsync(HttpContext context,
ILogger<OverrideResponseHandlerMiddleware> logger,
JsonSerializerOptions jsonSerializerOptions)
{
var originalResponseBodyStream = context.Response.Body;
await using var newResponseBodyStream = new MemoryStream();
context.Response.Body = newResponseBodyStream;
await this._next(context);
newResponseBodyStream.Seek(0, SeekOrigin.Begin);
var statusCode = context.Response.StatusCode;
var fuzzyBody = statusCode switch
{
401 => CreateFuzzyBody("NoAuthentication"),
403 => CreateFuzzyBody("NoAuthorization"),
_ => null
};
if (fuzzyBody != null)
{
var fuzzyData = JsonSerializer.Serialize(fuzzyBody, jsonSerializerOptions);
logger.LogInformation("Fuzzy data:{FuzzyData}", fuzzyData);
var realData = await new StreamReader(newResponseBodyStream).ReadToEndAsync();
logger.LogInformation("Read data:{RealData}", realData);
context.Response.Body = originalResponseBodyStream;
await context.Response.WriteAsync(fuzzyData);
}
else
{
await newResponseBodyStream.CopyToAsync(originalResponseBodyStream);
context.Response.Body = originalResponseBodyStream;
}
}
private static object CreateFuzzyBody(string failureCode)
{
return new
{
ErrorCode = failureCode,
ErrorMessage = "Please contact your administrator"
};
}
}
Middleware 的測試案例
新增一個 MsTest 測試專案並安裝以下套件
dotnet add package Microsoft.AspNetCore.TestHost --version 6.0.8
Middleware 的單元測試上一篇有比較完整的介紹,ASP.NET Core 6 Middleware 的單元測試 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
這裡我會使用 DefaultHttpContext 來完成這一次的演示
首先我用 DI Container,把我需要的物件都準好
private static IServiceProvider CreateServiceProvider()
{
var services = new ServiceCollection();
services.AddSingleton(p => CreateJsonSerializerOptions());
services.AddSingleton(p => LoggerFactory.Create(builder => { builder.AddConsole(); }));
services.AddSingleton(p => p.GetService<ILoggerFactory>().CreateLogger<OverrideResponseHandlerMiddleware>());
return services.BuildServiceProvider();
}
private static JsonSerializerOptions CreateJsonSerializerOptions()
{
return new()
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin,
UnicodeRanges.CjkUnifiedIdeographs),
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}
Middleware 需要下一關的 Next HttpContext,這裡回傳真實資料
private static Task CreateFakeNextContext(HttpContext context, object detailFailure, int statusCode)
{
context.Response.StatusCode = statusCode;
context.Response.WriteAsJsonAsync(detailFailure);
return Task.CompletedTask;
}
到時候需要確保 OverrideResponseHandlerMiddleware 順序是在 Middleware Response 的最後一個
我預期 HttpStatus = 200 不會覆寫訊息 { Code = "9527" },測試案例如下
[TestMethod]
public async Task 不模糊訊息()
{
var expected = @"{""code"":""9527""}";
var serviceProvider = CreateServiceProvider();
var jsonSerializerOptions = serviceProvider.GetService<JsonSerializerOptions>();
var logger = serviceProvider.GetService<ILogger<OverrideResponseHandlerMiddleware>>();
var target = new OverrideResponseHandlerMiddleware(nextContext =>
CreateFakeNextContext(nextContext, new { Code = "9527" }, StatusCodes.Status200OK));
var httpContext = new DefaultHttpContext
{
Response = { Body = new MemoryStream() }
};
await target.InvokeAsync(httpContext, logger, jsonSerializerOptions);
var response = httpContext.Response;
var stream = response.Body;
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
}
var actual = await new StreamReader(stream).ReadToEndAsync();
Assert.AreEqual(expected, actual);
}
當 HttpStatus = 403 覆寫訊息,不論本來回傳什麼,都會被覆寫訊息並寫入 log,測試案例如下
[TestMethod]
public async Task 模糊化未授權訊息()
{
var expected = @"{""errorCode"":""NoAuthorization"",""errorMessage"":""Please contact your administrator""}";
var httpContext = new DefaultHttpContext
{
Response = { Body = new MemoryStream() }
};
var serviceProvider = CreateServiceProvider();
var jsonSerializerOptions = serviceProvider.GetService<JsonSerializerOptions>();
var logger = serviceProvider.GetService<ILogger<OverrideResponseHandlerMiddleware>>();
var target = new OverrideResponseHandlerMiddleware(nextContext =>
CreateFakeNextContext(nextContext, new
{
ErrorCode = "NoAuthorization",
ErrorMessage = "No permission"
}, StatusCodes.Status403Forbidden));
await target.InvokeAsync(httpContext, logger, jsonSerializerOptions);
var response = httpContext.Response;
var stream = response.Body;
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
}
var actual = await new StreamReader(stream).ReadToEndAsync();
Assert.AreEqual(expected, actual);
}
範例位置
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET