解決在 Web API 使用 HttpClient / IHttpClientFactory 處理 Cookie 出現狀態錯亂的問題

日前,在工作上碰到了 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();
}

 

實驗開始

第一次

  1. api/demo1 帶著參數 headers[A]=123 送給 api/demo2
  2. api/demo2 收到 headers[B]==123 後,也只會收到這個 header,分別在 response.Headers[B]、[C]、[D],response.Cookies[B]、[C]、[D] 指定值
  3. 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 物件共用通常會導致程式碼不正確

出自:.NET 的 HttpClient 指導方針 - .NET | Microsoft Learn

 

以及,如果您的應用程式需要 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)
});
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 來傳遞狀態,但如果團隊說要,那麼就要注意用法

 

參考

 

範例位置

sample.dotblog/Http Client/Lab.HttpClientWithCookie at d51340288bcbd50972d43fa13050c17204421ce9 · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo