asp.net core 实现支持自定义 Content-Type
Intro
我们最近有一个原本是内网的服务要上公网,在公网上有一层 Cloudflare
作为网站的公网流量提供者,CloudFlare 会有一层防火墙拦截掉一些非法的请求,我们有一些 API 会提交一些 html 内容,经过 Cloudflare
的时候会被 Cloudflare
拦截,导致某些功能不能够正常使用,于是就想对提交的数据进行一个编码之后再提交,服务器端针对需要解码的请求进行解码再解析,我们新加了一个 Content-Type
的支持,编码后的数据使用新的 Content-Type
,对于不编码的数据依然可以工作,目前我们做了一个简单的 base64 编码,如果需要的话也可以实现复杂一些的加密、压缩等。
Basis
asp.net core 默认支持 JSON 请求,因为内置了针对 JSON 内容的 Formatter
,.NET Core 2.x 使用的是 Newtonsoft.Json
作为默认 JSON formatter,从 .NET Core 3.0 开始引入了 System.Text.Json
作为默认的 JSON formatter,如果要支持 XML 需要引入针对 XML 的 formatter,相应的如果需要增加其他类型的请求实现自己的 formatter 就可以了
Formatter 分为 InputFormatter
和 OutputFormatter
InputFormatter
用来解析请求Body
的数据,将请求参数映射到强类型的 model,Request Body => ValueOutputFormatter
用来将强类型的数据序列化成响应输出,Value => Response Body
Formatter 需要指定支持的 MediaType
,可以理解为请求类型,体现在请求头上,对于 InputFormatter
对应的就是 Content-Type
,对于 OutputFormatter
对应的是 Accept
,asp.net core 会根据请求信息来选择注册的 formatter。
Sample
先来看一下实现效果吧,实现效果如下:
swagger 的支持也算比较好了,在增加了新的 Content-Type
支持之后在 swagger 上可以看得到,而且可以切换请求的 Content-Type
,上图中的 text/base64-json
就是我自定义的一个 Content-Type
默认请求:
对原始请求进行 base64 编码,再请求:
Implement
实现代码如下:
public class Base64EncodedJsonInputFormatter : TextInputFormatter
{
public Base64EncodedJsonInputFormatter()
{
// 注册支持的 Content-Type
SupportedMediaTypes.Add("text/base64-json");
SupportedEncodings.Add(Encoding.UTF8);
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
try
{
using var reader = context.ReaderFactory(context.HttpContext.Request.Body, encoding);
var rawContent = await reader.ReadToEndAsync();
if (string.IsNullOrEmpty(rawContent))
{
return await InputFormatterResult.NoValueAsync();
}
var bytes = Convert.FromBase64String(rawContent);
var services = context.HttpContext.RequestServices;
var modelValue = await GetModelValue(services, bytes);
return await InputFormatterResult.SuccessAsync(modelValue);
async ValueTask<object> GetModelValue(IServiceProvider serviceProvider, byte[] stringBytes)
{
var newtonJsonOption = serviceProvider.GetService<IOptions<MvcNewtonsoftJsonOptions>>()?.Value;
if (newtonJsonOption is null)
{
await using var stream = new MemoryStream(stringBytes);
var result = await System.Text.Json.JsonSerializer.DeserializeAsync(stream, context.ModelType,
services.GetRequiredService<IOptions<JsonOptions>>().Value.JsonSerializerOptions);
return result;
}
var stringContent = encoding.GetString(bytes);
return Newtonsoft.Json.JsonConvert.DeserializeObject(stringContent, context.ModelType, newtonJsonOption.SerializerSettings);
}
}
catch (Exception e)
{
context.ModelState.TryAddModelError(string.Empty, e.Message);
return await InputFormatterResult.FailureAsync();
}
}
}
上述代码兼容了使用 System.Text.Json
和 Newtonsoft.Json
,在发生异常时将错误信息添加一个 ModelError
以便在前端可以得到错误信息的反馈,例如传一个不合法的 base64 字符串就会像下面这样:
实际使用的时候,只需要在 Startup
里配置一下就可以了,如:
services.AddControllers(options =>
{
options.InputFormatters.Add(new Base64EncodedJsonInputFormatter());
});
More
通过自定义 Content-Type
的支持我们可以无侵入的实现不同的请求内容,上面的示例代码可以在 Github 上获取 https://github.com/WeihanLi/SamplesInPractice/tree/master/AspNetCoreSample,可以根据自己的需要进行自定义