設定 HttpOnly 旗標的 Cookie,瀏覽器禁止客戶端腳本存取(防 XSS),但伺服器端代碼(如 WebBrowser 控件、爬蟲、自動化測試)常需完整讀取。WebBrowser.Document.Cookie 會忽略 HttpOnly Cookie。解決方案:P/Invoke 調用 Windows IE 核心庫 wininet.dll 的 InternetGetCookieEx。
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
internal static class NativeMethods
{
// 錯誤代碼
public enum ErrorFlags : uint
{
ERROR_INSUFFICIENT_BUFFER = 122,
ERROR_INVALID_PARAMETER = 87,
ERROR_NO_MORE_ITEMS = 259
}
// 網際網路旗標
public enum InternetFlags : uint
{
INTERNET_COOKIE_HTTPONLY = 0x2000, // 8192 - 需 IE 8+
INTERNET_COOKIE_THIRD_PARTY = 0x20000, // 131072
INTERNET_FLAG_RESTRICTED_ZONE = 0x10 // 16
}
// 關鍵 API:Unicode 版本
[SuppressUnmanagedCodeSecurity]
[SecurityCritical]
[DllImport("wininet.dll", EntryPoint = "InternetGetCookieExW",
CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool InternetGetCookieEx(
[In] string url,
[In] string cookieName,
[Out] StringBuilder cookieData,
[In, Out] ref uint pchCookieData,
uint flags,
IntPtr reserved);
}
public static class FullWebBrowserCookie
{
///
/// 取得指定 URI 的所有 Cookie(含 HttpOnly),回傳 Dictionary<Name, Value>
///
public static Dictionary<string, string> GetCookieList(Uri uri, bool throwIfNoCookie = false)
{
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string cookieStr = GetCookieInternal(uri, throwIfNoCookie);
if (string.IsNullOrEmpty(cookieStr)) return dict;
// 解析 "name1=value1; name2=value2" 格式
foreach (var pair in cookieStr.Split(';'))
{
var kv = pair.Split(new[] { '=' }, 2);
string key = kv[0].Trim();
string val = kv.Length > 1 ? kv[1].Trim() : string.Empty;
if (!dict.ContainsKey(key)) dict[key] = val;
}
return dict;
}
///
/// 取得單一 Cookie 值
///
public static string GetCookieValue(string key, Uri uri, bool throwIfNoCookie = false)
{
var dict = GetCookieList(uri, throwIfNoCookie);
return dict.TryGetValue(key, out var val) ? val : null;
}
private static string GetCookieInternal(Uri uri, bool throwIfNoCookie)
{
uint size = 0;
string url = UriToString(uri);
uint flags = (uint)NativeMethods.InternetFlags.INTERNET_COOKIE_HTTPONLY;
// 第一次呼叫:取得所需緩衝區大小
if (!NativeMethods.InternetGetCookieEx(url, null, null, ref size, flags, IntPtr.Zero))
{
int err = Marshal.GetLastWin32Error();
if (throwIfNoCookie || err != (int)NativeMethods.ErrorFlags.ERROR_NO_MORE_ITEMS)
throw new Win32Exception(err);
return null;
}
// 分配緩衝區並第二次呼叫取得資料
size++; // 空間預留
var sb = new StringBuilder((int)size);
if (!NativeMethods.InternetGetCookieEx(url, null, sb, ref size, flags, IntPtr.Zero))
{
int err = Marshal.GetLastWin32Error();
if (throwIfNoCookie || err != (int)NativeMethods.ErrorFlags.ERROR_NO_MORE_ITEMS)
throw new Win32Exception(err);
return null;
}
// 權限要求
DemandWebPermission(uri);
return sb.ToString();
}
private static void DemandWebPermission(Uri uri)
{
string uriStr = UriToString(uri);
if (uri.IsFile)
new System.Security.Permissions.FileIOPermission(
System.Security.Permissions.FileIOPermissionAccess.Read, uri.LocalPath).Demand();
else
new System.Net.WebPermission(System.Net.NetworkAccess.Connect, uriStr).Demand();
}
private static string UriToString(Uri uri)
{
if (uri == null) throw new ArgumentNullException(nameof(uri));
UriComponents comp = uri.IsAbsoluteUri ? UriComponents.AbsoluteUri : UriComponents.SerializationInfoString;
return new StringBuilder(uri.GetComponents(comp, UriFormat.SafeUnescaped), 2083).ToString();
}
}
// WinForms / WPF / Console 應用程式
var uri = new Uri("https://example.com");
try
{
var cookies = FullWebBrowserCookie.GetCookieList(uri);
foreach (var kv in cookies)
Console.WriteLine($"{kv.Key} = {kv.Value}");
// 取得特定 HttpOnly Cookie
string authToken = FullWebBrowserCookie.GetCookieValue("AuthToken", uri);
Console.WriteLine($"AuthToken: {authToken}");
}
catch (Win32Exception ex)
{
Console.WriteLine($"讀取失敗:{ex.Message} (ErrorCode: {ex.NativeErrorCode})");
}
catch (SecurityException ex)
{
Console.WriteLine($"權限不足:{ex.Message}");
}
| 場景 | 說明 |
|---|---|
| WebBrowser 控件自動化登入 | 登入後調用此類別取得完整 Cookie,供後續 HttpWebRequest/HttpClient 攜帶,實現「免密碼」後續請求。 |
| 爬蟲/測試工具 | 模擬瀏覽器行為,需完整 Cookie 容器(含 HttpOnly、Secure、SameSite)。 |
| 單點登出同步 | 讀取多個域名下的 Session Cookie,統一清除。 |
wininet.dll(IE 核心)。.NET Core / .NET 5+ 跨平台專案**無法使用**。INTERNET_COOKIE_HTTPONLY 旗標 IE 8 才支援。Set-Cookie 標頭存入容器,後續請求自動攜帶。推薦做法。CoreWebView2.CookieManager 完整管理 Cookie。driver.Manage().Cookies.AllCookies 取得所有 Cookie。透過 P/Invoke 調用 wininet.dll!InternetGetCookieExW 並傳遞 INTERNET_COOKIE_HTTPONLY 旗標,是傳統 .NET Framework + WebBrowser 控件環境下讀取 HttpOnly Cookie 的唯一可行方案。代碼需妥善處理緩衝區兩次呼叫、錯誤代碼、權限要求。對於新專案或跨平台需求,**請直接改用 HttpClient 自行管理 Cookie 或採用現代瀏覽器自動化庫**,避免綁定 IE 與 Windows 平台。