[英]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-Cookie
, Set-cookie
, set-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
的實例,並且不尊重通過IHttpResponseFeature
或HttpContext.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
}
這將在APIGatewayProxyFunction
的foreach (var kvp in responseFeatures.Headers)
塊內返回不同的Set-Cookie
標頭。
該解決方案已經過測試,目前似乎可以使用。 但是,沒有考慮任何極端情況或性能方面的考慮。 歡迎提出建議和改進。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.