asp.net core 實現支援自定義 Content-Type

WeihanLi發表於2021-03-13

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 分為 InputFormatterOutputFormatter

  • InputFormatter 用來解析請求 Body 的資料,將請求引數對映到強型別的 model,Request Body => Value
  • OutputFormatter 用來將強型別的資料序列化成響應輸出,Value => Response Body

Formatter 需要指定支援的 MediaType,可以理解為請求型別,體現在請求頭上,對於 InputFormatter 對應的就是 Content-Type ,對於 OutputFormatter 對應的是 Accept,asp.net core 會根據請求資訊來選擇註冊的 formatter。

Sample

先來看一下實現效果吧,實現效果如下:

swagger

swagger 的支援也算比較好了,在增加了新的 Content-Type 支援之後在 swagger 上可以看得到,而且可以切換請求的 Content-Type,上圖中的 text/base64-json 就是我自定義的一個 Content-Type

預設請求:

json-request

對原始請求進行 base64 編碼,再請求:

base64-json-request

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.JsonNewtonsoft.Json,在發生異常時將錯誤資訊新增一個 ModelError 以便在前端可以得到錯誤資訊的反饋,例如傳一個不合法的 base64 字串就會像下面這樣:

error

實際使用的時候,只需要在 Startup 裡配置一下就可以了,如:

services.AddControllers(options =>
    {
        options.InputFormatters.Add(new Base64EncodedJsonInputFormatter());
    });

More

通過自定義 Content-Type 的支援我們可以無侵入的實現不同的請求內容,上面的示例程式碼可以在 Github 上獲取 https://github.com/WeihanLi/SamplesInPractice/tree/master/AspNetCoreSample,可以根據自己的需要進行自定義

References

相關文章