日前,在工作上碰到了 HttpClient 處理 Cookie 時,用戶取得 Cookie 狀態錯移,就是 B 用戶拿到 A 用戶的 Cookie 狀態,追了一下,才發現是預設共用了 CookieContainer 了,要怎麼解這題??
開發環境
- Windows 11
- .NET 8
- Rider 2023.3
問題重現
前置準備
我建了 WebAppA (api/demo1)、WebAppB(api/demo2) 兩個站,主要的核心配置如下
- 分別配置 seq log
- 在 api/demo1 送命令給 api/demo2
- 在 api/demo2 觀察有沒有接收到非預期的 Cookie / Header,有收到就寫 Error Log,初期不確定不知道是誰的狀態亂了,所以觀察了 Header 以及 Cookie
api/demo2 片段程式碼如下,完整連結請參考
[HttpGet]
[Route("/api/demo1")]
public async Task<ActionResult> Get()
{
var requestId = this.HttpContext.TraceIdentifier;
//header
if (this.Request.Headers.Any())
{
this._logger.LogInformation("1.Id={@RequestId}, At 'Demo1', Receive request headers ={@Headers}", requestId,
this.Request.Headers);
}
//cookie
if (this.Request.Cookies.Any())
{
this._logger.LogInformation("2.Id={@RequestId}, At 'Demo1', Receive request cookie ={@Cookies}", requestId,
this.Request.Cookies);
}
var url = "api/Demo2";
var client = this._httpClientFactory.CreateClient("lab");
var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Headers =
{
{ "A", "123" },
}
};
var response = await client.SendAsync(request);
if (response.Headers.Any())
{
this._logger.LogInformation("3.Id={@RequestId}, At 'Demo1', Receive response headers ={@Headers}",
requestId,
response.Headers);
}
var responseContent = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(responseContent) == false)
{
this._logger.LogInformation("4.Id={@RequestId}, At 'Demo1', Receive response body ={@Body}",
requestId,
responseContent);
}
return this.Ok();
}
api/demo2 片段程式碼如下,完整連結請參考
[HttpGet]
[Route("/api/demo2")]
public async Task<ActionResult> Get()
{
var identifier = this.HttpContext.TraceIdentifier;
var cookies = this.ShouldWithoutCookies();
var headers = this.ShouldWithoutHeaders();
if (cookies.Any()
|| headers.Any())
{
// 找到非預期的 header/cookie 則回傳
return this.Ok(new
{
Headers = headers.Any() ? headers : null,
Cookies = cookies.Any() ? cookies : null,
});
}
if (this.Request.Headers.TryGetValue("A", out var context))
{
var data = context.FirstOrDefault();
//取得 Request Id
this._logger.LogInformation("1.Id={@RequestId}, At 'Demo2', Receive request headers[A] ={@Data}",
identifier,
data);
this.Response.Headers.Add("B", data);
this.Response.Cookies.Append("B", data);
//每一個請求都會有一個獨立的自動增量數字
lock (s_lock)
{
s_counter++;
this.Response.Headers.Add("C", s_counter.ToString());
this.Response.Cookies.Append("C", s_counter.ToString());
}
this.Response.Headers.Add("D", s_random.NextInt64(1000).ToString());
this.Response.Cookies.Append("D", s_random.NextInt64(1000).ToString());
}
return this.NoContent();
}
實驗開始
第一次
- api/demo1 帶著參數 headers[A]=123 送給 api/demo2
- api/demo2 收到 headers[B]==123 後,也只會收到這個 header,分別在 response.Headers[B]、[C]、[D],response.Cookies[B]、[C]、[D] 指定值
- api/demo1 會收到 api/demo2 回傳的 response.Headers[B]、[C]、[D],response.Cookies[B]、[C]、[D] 的內容
如下圖:
我把 api/demo1 收到的 Cookie 展開來看是 B=123,C=2,D=406
這結果很正常,再送一次。
這時 api/demo2 就收到剛剛 B=123,C=2,D=406
api/demo1 沒有特別的送出 Cookie,但是 api/demo2 卻收到 Cookie,這跟我所預期的不一樣,上述的步驟用 Swagger、Postman、cUrl 都會得到一樣的結果。
api/demo2 收到了上一次的 response.Cookies 的結果。
本來是懷疑是瀏覽器快取了 Cookies 的內容,我觀察瀏覽器也沒有任何的 Cookies 存放在瀏覽器。
後來,在官方文件找到了下面一段話
用 HttpMessageHandler 實例會導致共用 CookieContainer 物件。 未預期的 CookieContainer 物件共用通常會導致程式碼不正確
以及,如果您的應用程式需要 Cookie,最好避免在應用程式中使用 IHttpClientFactory。 如需管理用戶端的替代方式,請參閱使用 HTTP 用戶端的指導方針
出自:使用 HttpClientFactory 實作復原 HTTP 要求 - .NET | Microsoft Learn
除了不要用 IHttpClientFactory,還可以考慮停用自動處理 Cookie,上述內容都沒有寫到怎麼停用它,後來找到了這一篇
出自:.NET 的 HttpClient 指導方針 - .NET | Microsoft Learn
解決 Cookie 狀態錯亂
為了要解決該問題,我實驗了下列的方法,都沒有發生 Cookie 狀態錯亂的情況
- 使用 static HttpClient
- DI 使用 AddSingleton 注入 HttpClient
上述兩個方法都要注意 DNS 問題
builder.Services.AddSingleton(p => new HttpClient
{
BaseAddress = new Uri(serverUrl)
});
- 使用 IHttpClientFactory + ConfigurePrimaryHttpMessageHandler to disable automatic cookie
builder.Services.AddHttpClient("lab",
p => { p.BaseAddress = new Uri(serverUrl); })
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler()
{
// 改成 true,會快取 Cookie
UseCookies = false,
});
UseCookies = false ,也不會影響 HttpClient 送出 Cookie 的請求,使用 HttpRequestMessage 送 Cookie 的範例如下:
- Header Key 設定 Cookie
- Header Value 格式,cookie name=cookie value,多筆用分號隔開
var httpRequest = new HttpRequestMessage(HttpMethod.Get, "/test");
httpRequest.Headers.Add("Cookie", "cookie1=value1; cookie2=value2");
個人覺得在 Web API 可以不要用 Cookie 來傳遞狀態,但如果團隊說要,那麼就要注意用法
參考
- .NET 的 HttpClient 指導方針 - .NET | Microsoft Learn
- 使用 HttpClientFactory 實作復原 HTTP 要求 - .NET | Microsoft Learn
- Make HTTP requests using IHttpClientFactory in ASP.NET Core | Microsoft Learn
範例位置
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET