强制对 ASP.NET Core 2 中的已知标头进行特定大小写

Axe*_*ate 2 c# cookies aws-lambda aws-api-gateway asp.net-core-2.0

我有一个带有 Kestrel 的 ASP.NET Core 2 应用程序。该应用程序部署到 AWS Lambda/API Gateway。一切都按预期进行,除了一个导致一切不同的小细节。

对我的应用程序的某些请求需要发出多个与安全相关的Set-Cookie标头。由于 API Gateway 和 Lambda 之间传递数据的方式,重复的标头名称会连接在一起,这会导致标Set-Cookie头无效并且浏览器拒绝接受它。

克服此限制的建议解决方案是使用仅因大小写而异的多个标头名称:Set-Cookie, Set-cookie, set-cookie...

我知道这是一个 hacky 解决方案,但如果它有效,那么在 AWS 修复这个限制的同时,它应该已经足够好了。

但是,当使用 时HttpContext.Response.Headers.Add(name, value),已知的标头名称将被规范化并成为常规的重复标头。

是否有可能绕过这种常态化机制或以其他方式实现最终目标?

Cod*_*ler 5

当我开始研究这个问题时,我认为这很容易。经过半天的研究(太酷了,我正在度假),我终于可以分享结果了。

HttpContext.Response.Headers类型为IHeaderDictionary. 默认情况下,在 Kestrel 上的 ASP.NET Core 应用程序中,使用FrameResponseHeaders实现。主要逻辑位于FrameHeaders基类中。该标头字典针对设置/获取常用的标准 http 标头进行了高度优化。下面是处理设置 cookie(方法)的代码片段:AddValueFast

if ("Set-Cookie".Equals(key, StringComparison.OrdinalIgnoreCase))
{
    if ((_bits & 67108864L) == 0)
    {
        _bits |= 67108864L;
        _headers._SetCookie = value;
        return true;
    }
    return false;
}
Run Code Online (Sandbox Code Playgroud)

就用于键比较而言StringComparison.OrdinalIgnoreCase,您不能设置另一个仅大小写不同的 cookie 标头。这是有道理的,因为HTTP 标头不区分大小写。但让我们尝试克服它。

这里显而易见的解决方案是将 的实现替换IHeaderDictionary为区分大小写的实现。ASP.NET Core 为此包含很多接缝和扩展点,从包含可设置属性的IHttpResponseFeatureHeaders开始,到替换HttpContext.

不幸的是,在 Kestrel 上运行时,所有这些替代品都不起作用。如果您检查负责编写 HTTP 响应标头的FrameFrameResponseHeaders类的源代码,您将看到它自行创建 的实例,并且不尊重通过IHttpResponseFeature或设定的任何其他实例HttpContext.Response.Headers

protected FrameResponseHeaders FrameResponseHeaders { get; } = new FrameResponseHeaders();
Run Code Online (Sandbox Code Playgroud)

所以我们应该回到FrameResponseHeaders它的基FrameHeaders类并尝试调整它们的行为。

FrameResponseHeaders类使用已知标头的快速设置(见AddValueFast上文),但将所有其他未知标头存储在MaybeUnknown字段中:

protected Dictionary<string, StringValues> MaybeUnknown;
Run Code Online (Sandbox Code Playgroud)

其初始化为:

MaybeUnknown = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
Run Code Online (Sandbox Code Playgroud)

我们可以尝试绕过快速标头设置并将它们直接添加到MaybeUnknown字典中。然而,我们应该将使用比较器创建的字典替换StringComparer.OrdinalIgnoreCase为区分大小写的默认实现。

MaybeUnknown是一个受保护的字段,我们无法让 Kestrel 使用我们的自定义实现来保存类。这就是为什么我们被迫通过反射来设置这个字段。

我已将所有这些脏代码放入扩展类中FrameHeaders

public static class FrameHeadersExtensions
{
    public static void MakeCaseInsensitive(this FrameHeaders target)
    {
        var fieldInfo = GetDictionaryField(target.GetType());
        fieldInfo.SetValue(target, new Dictionary<string, StringValues>());
    }

    public static void AddCaseInsensitiveHeader(this FrameHeaders target, string key, string value)
    {
        var fieldInfo = GetDictionaryField(target.GetType());
        var values = (Dictionary<string, StringValues>)fieldInfo.GetValue(target);
        values.Add(key, value);
    }

    private static FieldInfo GetDictionaryField(Type headersType)
    {
        var fieldInfo = headersType.GetField("MaybeUnknown", BindingFlags.Instance | BindingFlags.NonPublic);
        if (fieldInfo == null)
        {
            throw new InvalidOperationException("Failed to get field info");
        }

        return fieldInfo;
    }
}
Run Code Online (Sandbox Code Playgroud)

MakeCaseInsensitive替换MaybeUnknown为区分大小写的字典。 AddCaseInsensitiveHeader绕过快速标头设置,将标头直接添加到MaybeUnknown字典中。

剩下的部分只是在控制器中的适当位置调用这些方法:

[Route("api/[controller]")]
public class TestController : Controller
{
    [NonAction]
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var responseHeaders = (FrameResponseHeaders)HttpContext.Response.Headers;
        responseHeaders.MakeCaseInsensitive();
    }

    // GET api/values
    [HttpGet]
    public string Get()
    {
        var responseHeaders = (FrameResponseHeaders)HttpContext.Response.Headers;
        responseHeaders.AddCaseInsensitiveHeader("Set-Cookie", "Cookies1");
        responseHeaders.AddCaseInsensitiveHeader("SET-COOKIE", "Cookies2");
        return "Hello";
    }
}
Run Code Online (Sandbox Code Playgroud)

这是结果标头集:

在此输入图像描述

所描述的解决方案是一个非常肮脏的黑客。它仅适用于 Kestrel,并且随着未来版本的发布,情况可能会发生变化。如果 Kestrel 完全支持 ASP.NET 接缝,一切都会变得更加容易和干净。但如果您目前没有其他选择,我希望这会对您有所帮助。