簡體   English   中英

強制使用ASP.NET Core 2中已知標頭的特定大小寫

[英]Force specific casing for known header in ASP.NET Core 2

我有一個帶有Kestrel的ASP.NET Core 2應用程序。 該應用程序已部署到AWS Lambda / API網關。 一切都按預期進行,除了細微的細節使一切有所不同。

對我的應用程序的某些請求需要發出多個與安全性相關的Set-Cookie標頭。 由於在API網關和Lambda之間傳遞數據的方式,重復的標頭名稱連接在一起,這使Set-Cookie標頭無效,瀏覽器拒絕接受它。

克服此限制的建議解決方案是使用僅根據大小寫而變化的多個標頭名稱: Set-CookieSet-cookieset-cookie ...

我知道這是一個駭人聽聞的解決方案,但是如果它可以工作,那么在AWS修復此限制的同時應該足夠好。

但是,當使用HttpContext.Response.Headers.Add(name, value) ,已知的標頭名稱將被規范化並成為常規的重復標頭。

是否有可能繞過這種規范化機制或以其他方式實現最終目標?

當我開始研究這個問題時,我認為這很容易。 經過半天的研究(太酷了,我正在度假),我終於可以分享結果了。

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;
}

就使用StringComparison.OrdinalIgnoreCase進行鍵比較而言,您不能設置僅因情況而異的另一個Cookie標頭。 這是有道理的,因為HTTP標頭不區分大小寫 但是,讓我們嘗試克服它。

顯而易見的解決方案是用區分大小寫的IHeaderDictionary替換IHeaderDictionary實現。 ASP.NET Core為此包含許多接縫和可擴展性點,從包含可設置的Headers屬性的IHttpResponseFeature開始,以替換HttpContext實現結尾。

不幸的是,當在Kestrel上運行時,所有這些替換都無法解決問題。 如果檢查負責編寫HTTP響應標頭的Frame類的源代碼,您會看到它自己創建了FrameResponseHeaders的實例,並且不尊重通過IHttpResponseFeatureHttpContext.Response.Headers設置的任何其他實例:

protected FrameResponseHeaders FrameResponseHeaders { get; } = new FrameResponseHeaders();

因此,我們應該返回FrameResponseHeaders及其基礎FrameHeaders類,並嘗試調整其行為。

FrameResponseHeaders類使用已知頭的快速設置(請參見上面的AddValueFast ),但將所有其他未知頭存儲在MaybeUnknown字段中:

protected Dictionary<string, StringValues> MaybeUnknown;

初始化為:

MaybeUnknown = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);

我們可以嘗試繞過快速頭設置並將其直接添加到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;
    }
}

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";
    }
}

這是設置的結果標頭:

在此處輸入圖片說明

描述的解決方案是一個非常骯臟的黑客。 它僅適用於Kestrel,將來的發行版可能會改變。 如果Kestrel完全支持ASP.NET接縫,一切將變得更加輕松和整潔。 但是,如果您暫時沒有其他選擇,希望對您有所幫助。

感謝@CodeFuller的迅速而徹底的答復。 但是,在深入研究Amazon.Lambda.AspNetCoreServer 源代碼之后 ,我意識到使用自定義IServer實現代替了Kestrel。

我將代碼放在APIGatewayProxyFunction內部,在其中將標頭復制到響應中並連接在一起:

foreach (var kvp in responseFeatures.Headers)
{
    if (kvp.Value.Count == 1)
    {
        response.Headers[kvp.Key] = kvp.Value[0];
    }
    else
    {
        response.Headers[kvp.Key] = string.Join(",", kvp.Value);
    }
    ...
}

但是,就像Kestrel一樣,該庫使用自己的IHttpResponseFeature實現。 它在多功能InvokeFeatures類中,該類直接實例化,不能通過配置替換。 但是, APIGatewayProxyFunction公開了一些虛擬的Post *方法,以在不同點修改請求/響應的某些部分。 不幸的是,沒有任何方法可以在將ASP.NET核心響應轉換為APIGatewayProxyResponse之前就進行APIGatewayProxyResponse (可能類似於PreMarshallResponseFeature嗎?),所以我能找到的最佳選擇是向PostCreateContext添加一些代碼:

var responseFeature = context.HttpContext.Features.Get<IHttpResponseFeature>();
responseFeature.Headers = new MyHeaderDictionary(responseFeature.Headers);

MyHeaderDictionary是圍繞一個包裝IHeaderDictionary其中I重寫IEnumerator<KeyValuePair<string, StringValues>> GetEnumerator()方法:

class MyHeaderDictionary : IHeaderDictionary
{
    private readonly IHeaderDictionary _inner;

    public MyHeaderDictionary(IHeaderDictionary inner)
    {
        _inner = inner;
    }

    public IEnumerator<KeyValuePair<string, StringValues>> GetEnumerator()
    {
        foreach (var kvp in _inner)
        {
            if (kvp.Key.Equals(HeaderNames.SetCookie) && kvp.Value.Count > 1)
            {
                int i = 0;
                foreach (var stringValue in kvp.Value)
                {
                    // Separate values as header names that differ by case
                    yield return new KeyValuePair<string, StringValues>(ModifiedHeaderNames[i], stringValue);
                    i++;
                }
            }
            else
            {
                yield return kvp;
            }
        }
    }

    // Implement all other IHeaderDictionary members as wrappers around _inner
}

這將在APIGatewayProxyFunctionforeach (var kvp in responseFeatures.Headers)塊內返回不同的Set-Cookie標頭。

該解決方案已經過測試,目前似乎可以使用。 但是,沒有考慮任何極端情況或性能方面的考慮。 歡迎提出建議和改進。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM