我一直想知道如何让HttpClientcookies像浏览器一样工作,然后在退出应用程序时保存,重启后继续使用。然后,最后,它找到了我,我做到了。
让它HttpClient像这样工作:
检查代码
public static class HttpManager
{
private static readonly HttpClientHandler handler = new HttpClientHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
AllowAutoRedirect = true
};
private static readonly HttpClient client = new HttpClient(handler)
{
DefaultRequestVersion = new Version(2, 0)
};
static HttpManager()
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0");
client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br");
}
// GET
public static async Task<string> GetPageAsync(string url)
{
using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
return await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
}
// POST
public static async Task<string> PostFormAsync(string url, Dictionary<string, string> data)
{
using HttpContent content = new FormUrlEncodedContent(data);
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
return await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
}
// Загрузить и расшифровать Cookie из файла
public static async Task LoadCookiesAsync(string filename)
{
if (File.Exists(filename))
{
using FileStream fs = File.OpenRead(filename);
byte[] IV = new byte[16];
fs.Read(IV);
byte[] protectedKey = new byte[178];
fs.Read(protectedKey);
using AesManaged aes = new AesManaged
{
Key = ProtectedData.Unprotect(protectedKey, IV, DataProtectionScope.CurrentUser),
IV = IV
};
using CryptoStream cs = new CryptoStream(fs, aes.CreateDecryptor(), CryptoStreamMode.Read, true);
CookieCollection cookies = await JsonSerializer.DeserializeAsync<CookieCollection>(cs);
foreach (Cookie cookie in cookies)
{
// не загружать, если кука заэкспайрилась
if (!cookie.Expired && (cookie.Expires == DateTime.MinValue || cookie.Expires > DateTime.Now))
handler.CookieContainer.Add(cookie);
}
}
}
// Зашифровать и сохранить Cookie в файл
public static async Task SaveCookiesAsync(string filename)
{
using AesManaged aes = new AesManaged();
using FileStream fs = File.Create(filename);
fs.Write(aes.IV);
fs.Write(ProtectedData.Protect(aes.Key, aes.IV, DataProtectionScope.CurrentUser));
using CryptoStream cs = new CryptoStream(fs, aes.CreateEncryptor(), CryptoStreamMode.Write, true);
await JsonSerializer.SerializeAsync(cs, handler.CookieContainer.GetAllCookies());
}
}
public static class CookieContainerExtensions
{
// Забирает все куки из контейнера
public static CookieCollection GetAllCookies(this CookieContainer container)
{
CookieCollection allCookies = new CookieCollection();
IDictionary domains = (IDictionary)container.GetType()
.GetRuntimeFields()
.FirstOrDefault(x => x.Name == "m_domainTable")
.GetValue(container);
foreach (object field in domains.Values)
{
IDictionary values = (IDictionary)field.GetType()
.GetRuntimeFields()
.FirstOrDefault(x => x.Name == "m_list")
.GetValue(field);
foreach (CookieCollection cookies in values.Values)
{
allCookies.Add(cookies);
}
}
return allCookies;
}
}
Cookie 被序列化为 Json,通过 DPAPI 使用密钥保护加密,并保存到磁盘。
使用已检查代码的可重现示例
使用登录名和密码对 StackOverflow 进行授权
using HtmlAgilityPack;
using Fizzler.Systems.HtmlAgilityPack;
private const string filename = "cookies.bin";
static async Task Main(string[] args)
{
await HttpManager.LoadCookiesAsync(filename);
string html = await HttpManager.GetPageAsync("https://ru.stackoverflow.com/");
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(html);
if (doc.DocumentNode.QuerySelector(".top-bar ol").HasClass("user-logged-out"))
{
html = await HttpManager.GetPageAsync("https://ru.stackoverflow.com/users/login?ssrc=head&returnurl=https%3a%2f%2fru.stackoverflow.com%2f");
doc = new HtmlDocument();
doc.LoadHtml(html);
string fkey = doc.DocumentNode.QuerySelector("form#login-form input[name=fkey]").Attributes["value"].Value;
string ssrc = doc.DocumentNode.QuerySelector("form#login-form input[name=ssrc]").Attributes["value"].Value;
Console.Write("Login: ");
string login = Console.ReadLine();
Console.Write("Password: ");
string password = ReadPassword();
Dictionary<string, string> formData = new Dictionary<string, string>
{
{ "fkey", fkey },
{ "ssrc", ssrc },
{ "email", login },
{ "password", password },
{ "oauth_version", "" },
{ "oauth_server", "" }
};
html = await HttpManager.PostFormAsync("https://ru.stackoverflow.com/users/login?ssrc=head&returnurl=https%3a%2f%2fru.stackoverflow.com%2f", formData);
doc = new HtmlDocument();
doc.LoadHtml(html);
}
string user = doc.DocumentNode.QuerySelector(".top-bar ol .my-profile span.v-visible-sr").InnerText;
Console.WriteLine(user);
string rep = HtmlEntity.DeEntitize(doc.DocumentNode.QuerySelector(".top-bar ol .my-profile div.-rep").Attributes["title"].Value);
Console.WriteLine(rep);
string badges = string.Join(Environment.NewLine, doc.DocumentNode.QuerySelectorAll(".top-bar ol .my-profile div.-badges span.v-visible-sr").Select(x => x.InnerText));
Console.WriteLine(badges);
await HttpManager.SaveCookiesAsync(filename);
Console.ReadKey();
}
private static string ReadPassword()
{
string password = string.Empty;
ConsoleKey key;
do
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
key = keyInfo.Key;
if (key == ConsoleKey.Backspace && password.Length > 0)
{
Console.Write("\b \b");
password = password[0..^1];
}
else if (!char.IsControl(keyInfo.KeyChar))
{
Console.Write("*");
password += keyInfo.KeyChar;
}
} while (key != ConsoleKey.Enter);
Console.WriteLine();
return password;
}
首次运行时的控制台输出要求提供凭据
Login: <my_email>@<censored>.ru
Password: ********************
aepot
ваша репутация: 4,904
2 золотых знака
5 серебряных знаков
25 бронзовых знаков
应用重启时输出到控制台,自动使用cookies进行授权
aepot
ваша репутация: 4,904
2 золотых знака
5 серебряных знаков
25 бронзовых знаков
无需检查可重现的示例,我知道它不考虑密码错误情况或帐户不支持密码登录。
顺便一提!为此,您需要在 StackExchange 帐户的安全设置中启用登录名和密码。
请看课程代码HttpManager,有什么需要改进或做不同的吗?我以前没有使用过 Cookie。
更新- 使用BinaryFormatter
在@ヒミコ的建议下,我尝试了一个使用. 它也有效。好处 - 现在您不需要额外的方法来提取 cookie,但它会“按原样”保存。我还没有弄清楚哪个更好,但微软斥责它不安全。BinaryFormatterCookieContainer BinaryFormatter
BinaryFormatter是不安全的,不能保证安全。有关详细信息,请参阅BinaryFormatter 安全指南。
public static class HttpManager
{
private static readonly HttpClientHandler handler = new HttpClientHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
AllowAutoRedirect = true
};
private static readonly HttpClient client = new HttpClient(handler)
{
DefaultRequestVersion = new Version(2, 0)
};
static HttpManager()
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0");
client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br");
}
public static async Task<string> GetPageAsync(string url)
{
using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
return await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
}
public static async Task<string> PostFormAsync(string url, Dictionary<string, string> data)
{
using HttpContent content = new FormUrlEncodedContent(data);
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
return await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
}
public static async Task LoadCookiesAsync(string filename)
{
if (File.Exists(filename))
{
using FileStream fs = File.OpenRead(filename);
byte[] IV = new byte[16];
await fs.ReadAsync(IV);
byte[] protectedKey = new byte[178];
await fs.ReadAsync(protectedKey);
using AesManaged aes = new AesManaged
{
Key = ProtectedData.Unprotect(protectedKey, IV, DataProtectionScope.CurrentUser),
IV = IV
};
using CryptoStream cs = new CryptoStream(fs, aes.CreateDecryptor(), CryptoStreamMode.Read, true);
handler.CookieContainer = new BinaryFormatter().Deserialize(cs) as CookieContainer ?? new CookieContainer();
}
}
public static async Task SaveCookiesAsync(string filename)
{
using AesManaged aes = new AesManaged();
using FileStream fs = File.Create(filename);
await fs.WriteAsync(aes.IV);
await fs.WriteAsync(ProtectedData.Protect(aes.Key, aes.IV, DataProtectionScope.CurrentUser));
using CryptoStream cs = new CryptoStream(fs, aes.CreateEncryptor(), CryptoStreamMode.Write, true);
new BinaryFormatter().Serialize(cs, handler.CookieContainer);
}
}
