NET 在 System.Net.* 命名空間中提供了各種類,用于通過標準網(wǎng)絡(luò)協(xié)議(如 HTTP 和 TCP/IP)進行通信。以下是關(guān)鍵組件的摘要:
本章中的 .NET 類型位于 System.Net.* 和 System.IO 命名空間中。
.NET 還提供對 FTP 的客戶端支持,但只能通過已從 .NET 6 標記為過時的類。如果需要使用 FTP,最好的選擇是使用 NuGet 庫,例如 FluentFTP。
說明了 .NET 網(wǎng)絡(luò)類型及其所在的通信層。大多數(shù)類型駐留在層或中。傳輸層定義了發(fā)送和接收字節(jié)(TCP 和 UDP)的基本協(xié)議;應(yīng)用層定義了為特定應(yīng)用設(shè)計的更高級別的協(xié)議,例如檢索網(wǎng)頁 (HTTP)、發(fā)送郵件 (SMTP) 以及在域名和 IP 地址 (DNS) 之間進行轉(zhuǎn)換。
在應(yīng)用程序?qū)泳幊掏ǔJ亲罘奖愕?但是,您可能希望直接在傳輸層工作的原因有幾個。一種是是否需要 .NET 中未提供的應(yīng)用程序協(xié)議(如 POP3)來檢索郵件。另一個是如果您想為特殊應(yīng)用程序(如點對點客戶端)發(fā)明自定義協(xié)議。
在應(yīng)用程序協(xié)議中,HTTP在對通用通信的適用性方面是特殊的。它的基本操作模式(“給我包含此 URL 的網(wǎng)頁”)很好地適應(yīng)了“讓我了解使用這些參數(shù)調(diào)用此終結(jié)點的結(jié)果”。(除了“get”動詞之外,還有“put”,“post”和“delete”,允許基于REST的服務(wù)。
HTTP 還具有一組豐富的功能,這些功能在多層業(yè)務(wù)應(yīng)用程序和面向服務(wù)的體系結(jié)構(gòu)中非常有用,例如用于身份驗證和加密、消息分塊、可擴展標頭和 Cookie 的協(xié)議,以及讓許多服務(wù)器應(yīng)用程序共享單個端口和 IP 地址的能力。由于這些原因,HTTP 在 .NET 中得到了很好的支持,既可以直接支持(如本章所述),也可以通過 Web API 和 ASP.NET Core 等技術(shù)在更高級別得到支持。
正如前面的討論所表明的那樣,網(wǎng)絡(luò)是一個充斥著首字母縮略詞的領(lǐng)域。我們在 中列出了最常見的。
網(wǎng)絡(luò)縮略語 | ||
縮寫 | 擴張 | 筆記 |
域名解析 | 域名服務(wù) | 在域名(例如 )和 IP 地址(例如 199.54.213.2)之間進行轉(zhuǎn)換 |
郵票 | 文件傳輸協(xié)議 | 用于發(fā)送和接收文件的基于互聯(lián)網(wǎng)的協(xié)議 |
HTTP | 超文本傳輸協(xié)議 | 檢索網(wǎng)頁并運行 Web 服務(wù) |
二世 | 互聯(lián)網(wǎng)信息服務(wù) | Microsoft的網(wǎng)絡(luò)服務(wù)器軟件 |
知識產(chǎn)權(quán) | 網(wǎng)際協(xié)議 | 低于 TCP 和 UDP 的網(wǎng)絡(luò)層協(xié)議 |
局域網(wǎng) | 局域網(wǎng) | 大多數(shù)局域網(wǎng)使用基于互聯(lián)網(wǎng)的協(xié)議,如TCP/IP |
流行 | 郵局協(xié)議 | 檢索互聯(lián)網(wǎng)郵件 |
休息 | 再現(xiàn)狀態(tài)轉(zhuǎn)移 | 一種流行的 Web 服務(wù)體系結(jié)構(gòu),在響應(yīng)中使用機器可遵循的鏈接,并且可以在基本 HTTP 上運行 |
短信通信 | 簡單郵件傳輸協(xié)議 | 發(fā)送互聯(lián)網(wǎng)郵件 |
技術(shù)合作計劃(TCP | 傳輸和控制協(xié)議 | 傳輸層互聯(lián)網(wǎng)協(xié)議,大多數(shù)更高層服務(wù)都在其上構(gòu)建 |
UDP | 通用數(shù)據(jù)報協(xié)議 | 用于低開銷服務(wù)(如 VoIP)的傳輸層互聯(lián)網(wǎng)協(xié)議 |
北卡羅來納大學(xué) | 通用命名約定 | \計算機\共享名\文件名 |
烏里 | 統(tǒng)一資源標識符 | 無處不在的資源命名系統(tǒng)(例如, 或mailto:) |
網(wǎng)址 | 統(tǒng)一資源定位器 | 技術(shù)含義(從使用中淡出):URI的子集;通俗含義:URI 的同義詞 |
要使通信正常工作,計算機或設(shè)備需要一個地址。互聯(lián)網(wǎng)使用兩種尋址系統(tǒng):
IPv4
目前占主導(dǎo)地位的尋址系統(tǒng);IPv4 地址的寬度為 32 位。當字符串格式時,IPv4 地址被寫入為四個點分隔的小數(shù)(例如,101.102.103.104)。地址在世界上可以是唯一的,也可以在特定中是唯一的(例如在公司網(wǎng)絡(luò)上)。
IPv6
較新的 128 位尋址系統(tǒng)。地址采用十六進制格式的字符串格式,帶有冒號分隔符(例如,[3EA0:FFFF:198A:E4A3:4FF2:54fA:41BC:8D31])。.NET 要求在地址兩邊添加方括號。
System.Net 命名空間中的 IPAddress 類表示任一協(xié)議中的地址。它有一個接受字節(jié)數(shù)組的構(gòu)造函數(shù)和一個接受正確格式字符串的靜態(tài) Parse 方法:
IPAddress a1 = new IPAddress (new byte[] { 101, 102, 103, 104 });
IPAddress a2 = IPAddress.Parse ("101.102.103.104");
Console.WriteLine (a1.Equals (a2)); // True
Console.WriteLine (a1.AddressFamily); // InterNetwork
IPAddress a3 = IPAddress.Parse
("[3EA0:FFFF:198A:E4A3:4FF2:54fA:41BC:8D31]");
Console.WriteLine (a3.AddressFamily); // InterNetworkV6
TCP 和 UDP 協(xié)議將每個 IP 地址分成 65,535 個端口,允許單個地址上的計算機運行多個應(yīng)用程序,每個應(yīng)用程序都在自己的端口上。許多應(yīng)用程序具有標準的默認端口分配;例如,HTTP 使用端口 80;SMTP 使用端口 25。
從 49152 到 65535 的 TCP 和 UDP 端口是正式未分配的,因此它們非常適合測試和小規(guī)模部署。
IP 地址和端口組合在 .NET 中由 IPEndPoint 類表示:
IPAddress a = IPAddress.Parse ("101.102.103.104");
IPEndPoint ep = new IPEndPoint (a, 222); // Port 222
Console.WriteLine (ep.ToString()); // 101.102.103.104:222
防火墻阻止端口。在許多企業(yè)環(huán)境中,只有少數(shù)端口處于打開狀態(tài),通常是端口 80(用于未加密的 HTTP)和端口 443(用于安全 HTTP)。
URI 是一個特殊格式的字符串,用于描述互聯(lián)網(wǎng)或 LAN 上的資源,例如網(wǎng)頁、文件或電子郵件地址。示例包括 、ftp://myisp/doc.txt 和 mailto:。確切的格式由 (IETF) 定義。
URI 可以分解為一系列元素,通常是、和。System 命名空間中的 Uri 類僅執(zhí)行此劃分,為每個元素公開一個屬性,如圖 所示。
當您需要驗證 URI 字符串的格式或?qū)?URI 拆分為其組成部分時,Uri 類非常有用。否則,可以將 URI 簡單地視為字符串 - 大多數(shù)網(wǎng)絡(luò)方法都會重載以接受 Uri 對象或字符串。
可以通過將以下任何字符串傳遞到其構(gòu)造函數(shù)中來構(gòu)造 Uri 對象:
文件和 UNC 路徑會自動轉(zhuǎn)換為 URI:添加“file:”協(xié)議,并將反斜杠轉(zhuǎn)換為正斜杠。Uri 構(gòu)造函數(shù)還會在創(chuàng)建 Uri 之前對字符串執(zhí)行一些基本的清理,包括將方案和主機名轉(zhuǎn)換為小寫以及刪除默認和空白端口號。如果提供不帶方案的 URI 字符串(如“”),則會引發(fā) UriFormatException。
Uri 具有 IsLoopback 屬性,該屬性指示 Uri 是否引用本地主機(IP 地址 127.0.0.1)和一個 IsFile 屬性,該屬性指示 Uri 引用本地路徑還是 UNC (IsUnc) 路徑(對于掛載在 文件系統(tǒng)中的 共享,IsUnc 報告 false)。如果 IsFile 返回 true ,則 LocalPath 屬性返回對本地操作系統(tǒng)友好的 AbsolutePath 版本(根據(jù)操作系統(tǒng)使用斜杠或反斜杠),您可以在該版本上調(diào)用 File.Open 。
Uri 的實例具有只讀屬性。要修改現(xiàn)有的 Uri,請實例化 UriBuilder 對象 - 該對象具有可寫屬性,可以通過其 Uri 屬性轉(zhuǎn)換回來。
Uri 還提供了比較和減去路徑的方法:
Uri info = new Uri ("http://www.domain.com:80/info/");
Uri page = new Uri ("http://www.domain.com/info/page.html");
Console.WriteLine (info.Host); // www.domain.com
Console.WriteLine (info.Port); // 80
Console.WriteLine (page.Port); // 80 (Uri knows the default HTTP port)
Console.WriteLine (info.IsBaseOf (page)); // True
Uri relative = info.MakeRelativeUri (page);
Console.WriteLine (relative.IsAbsoluteUri); // False
Console.WriteLine (relative.ToString()); // page.html
相對 Uri,例如本例中的 ,如果您調(diào)用除 IsAbsoluteUri 和 ToString() 之外的幾乎任何屬性或方法,則會引發(fā)異常。你可以直接實例化一個相對的 Uri,如下所示:
Uri u = new Uri ("page.html", UriKind.Relative);
尾部斜杠在 URI 中很重要,如果存在路徑組件,則服務(wù)器如何處理請求會有所不同。
例如,在傳統(tǒng)的Web服務(wù)器中,給定URI ,您可以期望HTTP Web服務(wù)器在站點Web文件夾中的子目錄中查找并返回默認文檔(通常是)。
如果沒有尾部斜杠,Web 服務(wù)器將直接在站點的根文件夾中查找一個名為 的文件(不帶擴展名),這通常不是您想要的。如果不存在此類文件,大多數(shù) Web 服務(wù)器將假定用戶鍵入錯誤,并將返回 301 錯誤,建議客戶端使用尾部斜杠重試。默認情況下,.NET HTTP 客戶端將以與 Web 瀏覽器相同的方式透明地響應(yīng) 301,方法是使用建議的 URI 重試。這意味著,如果在應(yīng)該包含尾部斜杠時省略了尾部斜杠,您的請求仍然有效,但會遭受不必要的額外往返。
Uri 類還提供了靜態(tài)輔助方法,例如 EscapeUriString() ,它通過將 ASCII 值大于 127 的所有字符轉(zhuǎn)換為十六進制表示形式,將字符串轉(zhuǎn)換為有效的 URL。CheckHostName() 和 CheckSchemeName() 方法接受字符串并檢查它對于給定屬性在語法上是否有效(盡管它們不嘗試確定主機或 URI 是否存在)。
HttpClient 類公開了一個用于 HTTP 客戶端操作的現(xiàn)代 API,取代了舊的 WebClient 和 WebRequest / WebResponse 類型(這些類型已被標記為過時)。
HttpClient 是為了響應(yīng)基于 HTTP 的 Web API 和 REST 服務(wù)的增長而編寫的,并且在處理比簡單地獲取網(wǎng)頁更復(fù)雜的協(xié)議時提供了良好的體驗。特別:
HttpClient 不支持進度報告。有關(guān)解決方案,請參閱 ,或通過 LINQPad 的交互式示例庫。
使用 HttpClient 的最簡單方法是實例化它,然后調(diào)用其 Get* 方法之一,傳入一個 URI:
string html = await new HttpClient().GetStringAsync ("http://linqpad.net");
(還有GetByteArrayAsync和GetStreamAsync。HttpClient 中的所有 I/O 綁定方法都是異步的。
與其WebRequest / WebResponse的前身不同,要獲得最佳性能 HttpClient ,重用相同的實例(否則DNS解析之類的事情可能會不必要地重復(fù),并且套接字保持打開的時間超過必要的時間)。HttpClient 允許并發(fā)操作,因此以下內(nèi)容是合法的,并且可以一次下載兩個網(wǎng)頁:
var client = new HttpClient();
var task1 = client.GetStringAsync ("http://www.linqpad.net");
var task2 = client.GetStringAsync ("http://www.albahari.com");
Console.WriteLine (await task1);
Console.WriteLine (await task2);
HttpClient 具有超時屬性和 BaseAddress 屬性,該屬性為每個請求添加前綴 URI。HttpClient有點像一個薄殼:您可能希望在此處找到的大多數(shù)其他屬性都是在另一個名為HttpClientHandler的類中定義的。要訪問這個類,你實例化它,然后將實例傳遞到 HttpClient 的構(gòu)造函數(shù)中:
var handler = new HttpClientHandler { UseProxy = false };
var client = new HttpClient (handler);
...
在此示例中,我們告訴處理程序禁用代理支持,這有時可以通過避免自動代理檢測的成本來提高性能。還有一些屬性可以控制 Cookie、自動重定向、身份驗證等(我們將在以下各節(jié)以及“使用 HTTP”中介紹這些屬性)。
GetStringAsync 、GetByteArrayAsync 和 GetStreamAsync 方法是調(diào)用更通用的 GetAsync 方法的便捷快捷方式,該方法返回:
var client = new HttpClient();
// The GetAsync method also accepts a CancellationToken.
HttpResponseMessage response = await client.GetAsync ("http://...");
response.EnsureSuccessStatusCode();
string html = await response.Content.ReadAsStringAsync();
HttpResponseMessage 公開了用于訪問標頭(請參閱和 HTTP 狀態(tài)代碼的屬性。不成功的狀態(tài)代碼(如 404(未找到))不會導(dǎo)致引發(fā)異常,除非您顯式調(diào)用 確保成功狀態(tài)代碼 。但是,通信或 DNS 錯誤確實會引發(fā)異常。
HttpContent 有一個用于寫入另一個流的 CopyToAsync 方法,該方法在將輸出寫入文件時很有用:
using (var fileStream = File.Create ("linqpad.html"))
await response.Content.CopyToAsync (fileStream);
GetAsync 是對應(yīng)于 HTTP 的四個動詞的四種方法之一(其他方法是 PostAsync、PutAsync 和 DeleteAsync)。稍后我們將在“上傳表單數(shù)據(jù)”中演示 PostAsync。
GetAsync 、PostAsync、PutAsync 和 DeleteAsync 都是調(diào)用 SendAsync 的快捷方式,SendAsync 是其他所有內(nèi)容都饋送到的單一低級方法。要使用它,您首先構(gòu)造一個 HttpRequestMessage :
var client = new HttpClient();
var request = new HttpRequestMessage (HttpMethod.Get, "http://...");
HttpResponseMessage response = await client.SendAsync (request);
response.EnsureSuccessStatusCode();
...
實例化 HttpRequestMessage 對象意味著您可以自定義請求的屬性,例如標頭(請參閱和內(nèi)容本身,從而允許您上傳數(shù)據(jù)。
實例化 HttpRequestMessage 對象后,可以通過分配其 Content 屬性來上載內(nèi)容。此屬性的類型是一個名為 HttpContent 的抽象類。.NET 包含以下用于不同類型內(nèi)容的具體子類(您也可以編寫自己的子類):
例如:
var client = new HttpClient (new HttpClientHandler { UseProxy = false });
var request = new HttpRequestMessage (
HttpMethod.Post, "http://www.albahari.com/EchoPost.aspx");
request.Content = new StringContent ("This is a test");
HttpResponseMessage response = await client.SendAsync (request);
response.EnsureSuccessStatusCode();
Console.WriteLine (await response.Content.ReadAsStringAsync());
我們之前說過,大多數(shù)用于自定義請求的屬性不是在 HttpClient 中定義的,而是在 HttpClientHandler 中定義的。后者實際上是抽象 HttpMessageHandler 類的子類,定義如下:
public abstract class HttpMessageHandler : IDisposable
{
protected internal abstract Task<HttpResponseMessage> SendAsync
(HttpRequestMessage request, CancellationToken cancellationToken);
public void Dispose();
protected virtual void Dispose (bool disposing);
}
SendAsync方法是從HttpClient的SendAsync方法調(diào)用的。
HttpMessageHandler非常簡單,可以輕松進行子類化,并為HttpClient提供了一個擴展點。
我們可以對 HttpMessageHandler 進行子類化,創(chuàng)建一個處理程序來幫助進行單元測試:
class MockHandler : HttpMessageHandler
{
Func <HttpRequestMessage, HttpResponseMessage> _responseGenerator;
public MockHandler
(Func <HttpRequestMessage, HttpResponseMessage> responseGenerator)
{
_responseGenerator = responseGenerator;
}
protected override Task <HttpResponseMessage> SendAsync
(HttpRequestMessage request, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var response = _responseGenerator (request);
response.RequestMessage = request;
return Task.FromResult (response);
}
}
它的構(gòu)造函數(shù)接受一個函數(shù),該函數(shù)告訴模擬者如何從請求生成響應(yīng)。這是最通用的方法,因為同一個處理程序可以測試多個請求。
SendAsync 是同步的,憑借 Task.FromResult 。我們本可以通過讓我們的響應(yīng)生成器返回一個 Task<HttpResponseMessage> 來保持異步性,但這是沒有意義的,因為我們可以預(yù)期模擬函數(shù)運行時間很短。以下是使用我們的模擬處理程序的方法:
var mocker = new MockHandler (request =>
new HttpResponseMessage (HttpStatusCode.OK)
{
Content = new StringContent ("You asked for " + request.RequestUri)
});
var client = new HttpClient (mocker);
var response = await client.GetAsync ("http://www.linqpad.net");
string result = await response.Content.ReadAsStringAsync();
Assert.AreEqual ("You asked for http://www.linqpad.net/", result);
(Assert.AreEqual是您希望在單元測試框架(如NUnit)中找到的方法。
您可以通過子類化 De委派處理程序 來創(chuàng)建調(diào)用另一個消息處理程序(生成處理程序鏈)。您可以使用它來實現(xiàn)自定義身份驗證、壓縮和加密協(xié)議。下面演示了一個簡單的日志記錄處理程序:
class LoggingHandler : DelegatingHandler
{
public LoggingHandler (HttpMessageHandler nextHandler)
{
InnerHandler = nextHandler;
}
protected async override Task <HttpResponseMessage> SendAsync
(HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine ("Requesting: " + request.RequestUri);
var response = await base.SendAsync (request, cancellationToken);
Console.WriteLine ("Got response: " + response.StatusCode);
return response;
}
}
請注意,我們在覆蓋 SendAsync 時保持了異步。在重寫任務(wù)返回方法時引入異步修飾符是完全合法的,在這種情況下是可取的。
比寫入控制臺更好的解決方案是讓構(gòu)造函數(shù)接受某種日志記錄對象。更好的辦法是接受幾個 Action<T> 委托,告訴它如何記錄請求和響應(yīng)對象。
是可以路由 HTTP 請求的中介。組織有時會將代理服務(wù)器設(shè)置為員工訪問互聯(lián)網(wǎng)的唯一方式,主要是因為它簡化了安全性。代理有自己的地址,可以要求身份驗證,以便只有 LAN 上的選定用戶才能訪問互聯(lián)網(wǎng)。
要將代理與 HttpClient 一起使用,首先創(chuàng)建一個 HttpClientHandler 并分配其 Proxy 屬性,然后將其饋送到 HttpClient 的構(gòu)造函數(shù)中:
WebProxy p = new WebProxy ("192.178.10.49", 808);
p.Credentials = new NetworkCredential ("username", "password", "domain");
var handler = new HttpClientHandler { Proxy = p };
var client = new HttpClient (handler);
...
HttpClientHandler 還有一個 UseProxy 屬性,您可以將其分配給 false,而不是清空 Proxy 屬性以阻止自動檢測。
如果在構(gòu)造 NetworkCredential 時提供域,則使用基于 Windows 的身份驗證協(xié)議。若要使用當前經(jīng)過身份驗證的 Windows 用戶,請將靜態(tài) CredentialCache.DefaultNetworkCredentials 值分配給代理的 Credentials 屬性。
作為重復(fù)設(shè)置代理的替代方法,您可以按如下方式設(shè)置全局默認值:
HttpClient.DefaultWebProxy = myWebProxy;
您可以向 HttpClient 提供用戶名和密碼,如下所示:
string username = "myuser";
string password = "mypassword";
var handler = new HttpClientHandler();
handler.Credentials = new NetworkCredential (username, password);
var client = new HttpClient (handler);
...
這適用于基于對話框的身份驗證協(xié)議(如基本和摘要),并且可通過 AuthenticationManager 類進行擴展。它還支持 Windows NTLM 和 Kerberos(如果在構(gòu)造 NetworkCredential 對象時包含域名)。如果要使用當前經(jīng)過身份驗證的 Windows 用戶,可以將“憑據(jù)”屬性保留為空,而是將“使用默認憑據(jù)”設(shè)置為 true 。
當您提供憑據(jù)時,HttpClient 會自動協(xié)商兼容的協(xié)議。在某些情況下,可以選擇:例如,如果檢查來自Microsoft Exchange 服務(wù)器 Web 郵件頁面的初始響應(yīng),則它可能包含以下標頭:
HTTP/1.1 401 Unauthorized
Content-Length: 83
Content-Type: text/html
Server: Microsoft-IIS/6.0
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="exchange.somedomain.com"
X-Powered-By: ASP.NET
Date: Sat, 05 Aug 2006 12:37:23 GMT
401 代碼表示需要授權(quán);“WWW 身份驗證”標頭指示理解的身份驗證協(xié)議。但是,如果使用正確的用戶名和密碼配置 HttpClientHandler,則此消息將對你隱藏,因為運行時通過選擇兼容的身份驗證協(xié)議,然后使用額外的標頭重新提交原始請求來自動響應(yīng)。下面是一個示例:
Authorization: Negotiate TlRMTVNTUAAABAAAt5II2gjACDArAAACAwACACgAAAAQ
ATmKAAAAD0lVDRdPUksHUq9VUA==
此機制提供透明度,但會為每個請求生成額外的往返行程。通過將 HttpClientHandler 上的 PreAuthenticate 屬性設(shè)置為 true 來避免對同一 URI 的后續(xù)請求進行額外的往返。
您可以使用憑據(jù)緩存對象強制使用特定的身份驗證協(xié)議。憑據(jù)緩存包含一個或多個 NetworkCredential 對象,每個對象都以特定協(xié)議和 URI 前綴為密鑰。例如,您可能希望在登錄 Exchange Server 時避免使用基本協(xié)議,因為它以純文本形式傳輸密碼:
CredentialCache cache = new CredentialCache();
Uri prefix = new Uri ("http://exchange.somedomain.com");
cache.Add (prefix, "Digest", new NetworkCredential ("joe", "passwd"));
cache.Add (prefix, "Negotiate", new NetworkCredential ("joe", "passwd"));
var handler = new HttpClientHandler();
handler.Credentials = cache;
...
身份驗證協(xié)議指定為字符串。有效值包括:
Basic, Digest, NTLM, Kerberos, Negotiate
在這種特殊情況下,它將選擇協(xié)商,因為服務(wù)器在其身份驗證標頭中未指示它支持 Digest。協(xié)商是一種 Windows 協(xié)議,目前歸結(jié)為 Kerberos 或 NTLM,具體取決于服務(wù)器的功能,但在部署未來安全標準時可確保應(yīng)用程序的向前兼容性。
靜態(tài) CredentialCache.DefaultNetworkCredentials 屬性允許您將當前經(jīng)過身份驗證的 Windows 用戶添加到憑據(jù)緩存中,而無需指定密碼:
cache.Add (prefix, "Negotiate", CredentialCache.DefaultNetworkCredentials);
另一種身份驗證方法是直接設(shè)置身份驗證標頭:
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue ("Basic",
Convert.ToBase64String (Encoding.UTF8.GetBytes ("username:password")));
...
此策略也適用于自定義身份驗證系統(tǒng),如 OAuth。
HttpClient 允許您向請求添加自定義 HTTP 標頭,以及在響應(yīng)中枚舉標頭。標頭只是包含元數(shù)據(jù)(如消息內(nèi)容類型或服務(wù)器軟件)的鍵/值對。HttpClient 公開具有標準 HTTP 標頭屬性的強類型集合。屬性適用于應(yīng)用于每個請求的標頭:
var client = new HttpClient (handler);
client.DefaultRequestHeaders.UserAgent.Add (
new ProductInfoHeaderValue ("VisualStudio", "2022"));
client.DefaultRequestHeaders.Add ("CustomHeader", "VisualStudio/2022");
但是,類上的 Headers 屬性用于特定于請求的標頭。
查詢字符串只是附加到帶有問號的 URI 的字符串,用于將簡單數(shù)據(jù)發(fā)送到服務(wù)器。可以使用以下語法在查詢字符串中指定多個鍵/值對:
?key1=value1&key2=value2&key3=value3...
下面是一個帶有查詢字符串的 URI:
string requestURI = "http://www.google.com/search?q=HttpClient&hl=fr";
如果您的查詢可能包含符號或空格,則可以使用 Uri 的 EscapeDataString 方法創(chuàng)建一個合法的 URI:
string search = Uri.EscapeDataString ("(HttpClient or HttpRequestMessage)");
string language = Uri.EscapeDataString ("fr");
string requestURI = "http://www.google.com/search?q=" + search +
"&hl=" + language;
此生成的 URI 為:
http://www.google.com/search?q=(HttpClient%20OR%20HttpRequestMessage)&hl=fr
(EscapeDataString 與 EscapeUriString 類似,不同之處在于它還對 & 和 = 等字符進行編碼,否則會弄亂查詢字符串。
若要上載 HTML 表單數(shù)據(jù),請創(chuàng)建并填充 FormUrlEncodedContent 對象。然后,可以將其傳遞到 PostAsync 方法中,也可以將其分配給請求的 Content 屬性:
string uri = "http://www.albahari.com/EchoPost.aspx";
var client = new HttpClient();
var dict = new Dictionary<string,string>
{
{ "Name", "Joe Albahari" },
{ "Company", "O'Reilly" }
};
var values = new FormUrlEncodedContent (dict);
var response = await client.PostAsync (uri, values);
response.EnsureSuccessStatusCode();
Console.WriteLine (await response.Content.ReadAsStringAsync());
Cookie 是 HTTP 服務(wù)器在響應(yīng)標頭中發(fā)送到客戶端的名稱/值字符串對。Web 瀏覽器客戶端通常會記住 Cookie,并在每次后續(xù)請求(到同一地址)中將它們重播到服務(wù)器,直到它們到期。Cookie 允許服務(wù)器知道它是在與一分鐘前還是昨天的同一客戶端通信,而無需在 URI 中提供混亂的查詢字符串。
默認情況下,HttpClient 會忽略從服務(wù)器接收的任何 Cookie。要接受 cookie,請創(chuàng)建一個 CookieContainer 對象并為其分配一個 HttpClientHandler:
var cc = new CookieContainer();
var handler = new HttpClientHandler();
handler.CookieContainer = cc;
var client = new HttpClient (handler);
...
要在將來的請求中重播收到的 Cookie,只需再次使用相同的 CookieContainer 對象即可。或者,您可以從新的 CookieContainer 開始,然后手動添加 cookie,如下所示:
Cookie c = new Cookie ("PREF",
"ID=6b10df1da493a9c4:TM=1179...",
"/",
".google.com");
freshCookieContainer.Add (c);
第三個和第四個參數(shù)指示發(fā)起方的路徑和域。客戶端上的 CookieContainer 可以容納來自許多不同位置的 Cookie;HttpClient 僅發(fā)送路徑和域與服務(wù)器路徑和域匹配的 cookie。
如果需要在 .NET 6 中編寫 HTTP 服務(wù)器,另一種更高級別的方法是使用最小 API ASP.NET。以下是入門所需的全部內(nèi)容:
var app = WebApplication.CreateBuilder().Build();
app.MapGet ("/", () => "Hello, world!");
app.Run();
您可以使用 HttpListener 類編寫自己的 .NET HTTP 服務(wù)器。下面是一個簡單的服務(wù)器,它偵聽端口 51111,等待單個客戶端請求,然后返回一行回復(fù):
using var server = new SimpleHttpServer();
// Make a client request:
Console.WriteLine (await new HttpClient().GetStringAsync
("http://localhost:51111/MyApp/Request.txt"));
class SimpleHttpServer : IDisposable
{
readonly HttpListener listener = new HttpListener();
public SimpleHttpServer() => ListenAsync();
async void ListenAsync()
{
listener.Prefixes.Add ("http://localhost:51111/MyApp/"); // Listen on
listener.Start(); // port 51111
// Await a client request:
HttpListenerContext context = await listener.GetContextAsync();
// Respond to the request:
string msg = "You asked for: " + context.Request.RawUrl;
context.Response.ContentLength64 = Encoding.UTF8.GetByteCount (msg);
context.Response.StatusCode = (int)HttpStatusCode.OK;
using (Stream s = context.Response.OutputStream)
using (StreamWriter writer = new StreamWriter (s))
await writer.WriteAsync (msg);
}
public void Dispose() => listener.Close();
}
OUTPUT: You asked for: /MyApp/Request.txt
在Windows上,HttpListener在內(nèi)部不使用.NET Socket對象;相反,它調(diào)用Windows HTTP Server API。這允許計算機上的許多應(yīng)用程序偵聽相同的 IP 地址和端口,只要每個應(yīng)用程序注冊不同的地址前綴即可。在我們的示例中,我們注冊了前綴 http://localhost/myapp,因此另一個應(yīng)用程序可以自由偵聽另一個前綴(如 http://localhost/anotherapp)上的同一 IP 和端口。這是有價值的,因為在公司防火墻上打開新端口在政治上可能很困難。
當您調(diào)用 GetContext 時,HttpListener 會等待下一個客戶端請求,返回具有請求和響應(yīng)屬性的對象。每個都類似于客戶端請求或響應(yīng),但從服務(wù)器的角度來看。例如,您可以讀取和寫入標頭和 Cookie 到請求和響應(yīng)對象,就像在客戶端一樣。
您可以根據(jù)預(yù)期的客戶端受眾選擇完全支持 HTTP 協(xié)議功能的程度。至少應(yīng)設(shè)置每個請求的內(nèi)容長度和狀態(tài)代碼。
這是一個非常簡單的網(wǎng)頁服務(wù)器,
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
class WebServer
{
HttpListener _listener;
string _baseFolder; // Your web page folder.
public WebServer (string uriPrefix, string baseFolder)
{
_listener = new HttpListener();
_listener.Prefixes.Add (uriPrefix);
_baseFolder = baseFolder;
}
public async void Start()
{
_listener.Start();
while (true)
try
{
var context = await _listener.GetContextAsync();
Task.Run (() => ProcessRequestAsync (context));
}
catch (HttpListenerException) { break; } // Listener stopped.
catch (InvalidOperationException) { break; } // Listener stopped.
}
public void Stop() => _listener.Stop();
async void ProcessRequestAsync (HttpListenerContext context)
{
try
{
string filename = Path.GetFileName (context.Request.RawUrl);
string path = Path.Combine (_baseFolder, filename);
byte[] msg;
if (!File.Exists (path))
{
Console.WriteLine ("Resource not found: " + path);
context.Response.StatusCode = (int) HttpStatusCode.NotFound;
msg = Encoding.UTF8.GetBytes ("Sorry, that page does not exist");
}
else
{
context.Response.StatusCode = (int) HttpStatusCode.OK;
msg = File.ReadAllBytes (path);
}
context.Response.ContentLength64 = msg.Length;
using (Stream s = context.Response.OutputStream)
await s.WriteAsync (msg, 0, msg.Length);
}
catch (Exception ex) { Console.WriteLine ("Request error: " + ex); }
}
}
以下代碼啟動了操作:
// Listen on port 51111, serving files in d:\webroot:
var server = new WebServer ("http://localhost:51111/", @"d:\webroot");
try
{
server.Start();
Console.WriteLine ("Server running... press Enter to stop");
Console.ReadLine();
}
finally { server.Stop(); }
您可以使用任何 Web 瀏覽器在客戶端對此進行測試;在這種情況下,URI 將 http://localhost:51111/ 加上網(wǎng)頁的名稱。
如果其他軟件競爭同一端口,HttpListener 將不會啟動(除非該軟件也使用 Windows HTTP Server API)。可能偵聽默認端口 80 的應(yīng)用程序示例包括 Web 服務(wù)器或?qū)Φ瘸绦颍ㄈ?Skype)。
我們對異步函數(shù)的使用使該服務(wù)器具有可擴展性和效率。但是,從用戶界面 (UI) 線程開始會阻礙可伸縮性,因為對于每個,執(zhí)行會在每次等待后反彈回 UI 線程。鑒于我們沒有共享狀態(tài),產(chǎn)生這樣的開銷特別沒有意義,因此在 UI 場景中,我們會像這樣離開 UI 線程。
Task.Run (Start);
或者在調(diào)用 GetContextAsync 后調(diào)用 ConfigureAwait(false)。
請注意,我們使用 Task.Run 來調(diào)用 ProcessRequestAsync,即使該方法已經(jīng)是異步的。這允許調(diào)用方處理另一個請求,而不必首先等待方法的同步階段(直到第一個等待)。
靜態(tài) Dns 類封裝 DNS,該 DNS 在原始 IP 地址(如 66.135.192.87)和人類友好域名(如 )之間進行轉(zhuǎn)換。
方法從域名轉(zhuǎn)換為 IP 地址(或多個地址):
foreach (IPAddress a in Dns.GetHostAddresses ("albahari.com"))
Console.WriteLine (a.ToString()); // 205.210.42.167
GetHostEntry 方法則相反,從地址轉(zhuǎn)換為域名:
IPHostEntry entry = Dns.GetHostEntry ("205.210.42.167");
Console.WriteLine (entry.HostName); // albahari.com
GetHostEntry 還接受 IPAddress 對象,因此您可以將 IP 地址指定為字節(jié)數(shù)組:
IPAddress address = new IPAddress (new byte[] { 205, 210, 42, 167 });
IPHostEntry entry = Dns.GetHostEntry (address);
Console.WriteLine (entry.HostName); // albahari.com
當您使用 WebRequest 或 TcpClient 等類時,域名會自動解析為 IP 地址。但是,如果您計劃在應(yīng)用程序的生命周期內(nèi)向同一地址發(fā)出許多網(wǎng)絡(luò)請求,則有時可以通過首先使用 Dns 將域名顯式轉(zhuǎn)換為 IP 地址,然后從該點開始直接與 IP 地址通信來提高性能。這避免了重復(fù)往返解析相同的域名,并且在傳輸層(通過 TcpClient 、UdpClient 或 Socket )處理時可能會有所幫助。
DNS 類還提供可等待的基于任務(wù)的異步方法:
foreach (IPAddress a in await Dns.GetHostAddressesAsync ("albahari.com"))
Console.WriteLine (a.ToString());
命名空間中的 SmtpClient 類允許您通過無處不在的簡單郵件傳輸協(xié)議 (SMTP) 發(fā)送郵件。要發(fā)送簡單的文本消息,請實例化 SmtpClient ,將其 Host 屬性設(shè)置為 SMTP 服務(wù)器地址,然后調(diào)用 發(fā)送 :
SmtpClient client = new SmtpClient();
client.Host = "mail.myserver.com";
client.Send ("from@adomain.com", "to@adomain.com", "subject", "body");
構(gòu)造 MailMessage 對象會公開更多選項,包括添加附件的功能:
SmtpClient client = new SmtpClient();
client.Host = "mail.myisp.net";
MailMessage mm = new MailMessage();
mm.Sender = new MailAddress ("kay@domain.com", "Kay");
mm.From = new MailAddress ("kay@domain.com", "Kay");
mm.To.Add (new MailAddress ("bob@domain.com", "Bob"));
mm.CC.Add (new MailAddress ("dan@domain.com", "Dan"));
mm.Subject = "Hello!";
mm.Body = "Hi there. Here's the photo!";
mm.IsBodyHtml = false;
mm.Priority = MailPriority.High;
Attachment a = new Attachment ("photo.jpg",
System.Net.Mime.MediaTypeNames.Image.Jpeg);
mm.Attachments.Add (a);
client.Send (mm);
為了阻止垃圾郵件發(fā)送者,互聯(lián)網(wǎng)上的大多數(shù)SMTP服務(wù)器僅接受來自經(jīng)過身份驗證的連接的連接,并要求通過SSL進行通信。
var client = new SmtpClient ("smtp.myisp.com", 587)
{
Credentials = new NetworkCredential ("me@myisp.com", "MySecurePass"),
EnableSsl = true
};
client.Send ("me@myisp.com", "someone@somewhere.com", "Subject", "Body");
Console.WriteLine ("Sent");
通過更改 DeliveryMethod 屬性,可以指示 SmtpClient 改用 IIS 發(fā)送郵件,或者只是將每封郵件寫入指定目錄中的 文件。這在開發(fā)過程中可能很有用。
SmtpClient client = new SmtpClient();
client.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
client.PickupDirectoryLocation = @"c:\mail";
TCP 和 UDP 構(gòu)成了傳輸層協(xié)議,大多數(shù)互聯(lián)網(wǎng)和 LAN 服務(wù)都在其上構(gòu)建。HTTP(版本 2 及更低版本)、FTP 和 SMTP 使用 TCP;DNS 和 HTTP 版本 3 使用 UDP。TCP 是面向連接的,包括可靠性機制;UDP 是無連接的,開銷較低,并支持廣播。使用UDP,IP語音(VoIP)也是如此。
與較高層相比,傳輸層提供了更大的靈活性,并可能提高性能,但它要求您自己處理身份驗證和加密等任務(wù)。
使用 .NET 中的 TCP,您可以選擇更易于使用的 TcpClient 和 TcpListener 外觀類,也可以選擇功能豐富的 Socket 類。 (實際上,您可以混合搭配,因為 TcpClient 通過 Client 屬性公開基礎(chǔ)套接字對象。Socket 類公開了更多的配置選項,并允許直接訪問網(wǎng)絡(luò)層 (IP) 和非基于 Internet 的協(xié)議,例如 Novell 的 SPX/IPX。
與其他協(xié)議一樣,TCP 區(qū)分客戶端和服務(wù)器:客戶端發(fā)起請求,而服務(wù)器等待請求。下面是同步 TCP 客戶端請求的基本結(jié)構(gòu):
using (TcpClient client = new TcpClient())
{
client.Connect ("address", port);
using (NetworkStream n = client.GetStream())
{
// Read and write to the network stream...
}
}
TcpClient 的連接方法會阻塞,直到建立連接(ConnectAsync 是異步等價物)。然后,NetworkStream提供了一種雙向通信方式,用于從服務(wù)器發(fā)送和接收字節(jié)的數(shù)據(jù)。
一個簡單的TCP服務(wù)器如下所示:
TcpListener listener = new TcpListener (<ip address>, port);
listener.Start();
while (keepProcessingRequests)
using (TcpClient c = listener.AcceptTcpClient())
using (NetworkStream n = c.GetStream())
{
// Read and write to the network stream...
}
listener.Stop();
TcpListener 需要偵聽的本地 IP 地址(例如,具有兩個網(wǎng)卡的計算機可以有兩個地址)。您可以使用 IPAddress.Any 指示它偵聽所有(或唯一)本地 IP 地址。AcceptTcpClient 阻塞,直到收到客戶端請求(同樣,還有一個異步版本),此時我們調(diào)用 GetStream ,就像在客戶端一樣。
在傳輸層工作時,您需要決定誰何時通話以及通話多長時間的協(xié)議,就像使用對講機一樣。如果雙方同時交談或傾聽,溝通就會中斷!
讓我們發(fā)明一個協(xié)議,在這個協(xié)議中,客戶端首先說“你好”,然后服務(wù)器通過說“你好馬上回來!代碼如下:
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
new Thread (Server).Start(); // Run server method concurrently.
Thread.Sleep (500); // Give server time to start.
Client();
void Client()
{
using (TcpClient client = new TcpClient ("localhost", 51111))
using (NetworkStream n = client.GetStream())
{
BinaryWriter w = new BinaryWriter (n);
w.Write ("Hello");
w.Flush();
Console.WriteLine (new BinaryReader (n).ReadString());
}
}
void Server() // Handles a single client request, then exits.
{
TcpListener listener = new TcpListener (IPAddress.Any, 51111);
listener.Start();
using (TcpClient c = listener.AcceptTcpClient())
using (NetworkStream n = c.GetStream())
{
string msg = new BinaryReader (n).ReadString();
BinaryWriter w = new BinaryWriter (n);
w.Write (msg + " right back!");
w.Flush(); // Must call Flush because we're not
} // disposing the writer.
listener.Stop();
}
// OUTPUT: Hello right back!
在此示例中,我們使用 localhost 環(huán)回在同一臺計算機上運行客戶端和服務(wù)器。我們?nèi)我膺x擇了未分配范圍內(nèi)的端口(高于 49152),并使用 BinaryWriter 和 BinaryReader 對文本消息進行編碼。我們避免關(guān)閉或處置讀取器和編寫器,以便在我們的對話完成之前保持底層 NetworkStream 打開。
BinaryReader 和 BinaryWriter 似乎是讀取和寫入字符串的奇怪選擇。但是,它們比StreamReader和StreamWriter有一個主要優(yōu)勢:它們在字符串前面加上一個指示長度的整數(shù),因此BinaryReader總是確切地知道要讀取多少字節(jié)。如果你調(diào)用StreamReader.ReadToEnd,你可能會無限期地阻止,因為NetworkStream沒有終點!只要連接處于打開狀態(tài),網(wǎng)絡(luò)流就永遠無法確定客戶端不會發(fā)送更多數(shù)據(jù)。
StreamReader實際上完全超出了NetworkStream的界限,即使你只打算調(diào)用ReadLine。這是因為 StreamReader 具有預(yù)讀緩沖區(qū),這可能導(dǎo)致它讀取的字節(jié)數(shù)超過當前可用的字節(jié)數(shù),從而無限期阻塞(或直到套接字超時)。其他流(如 FileStream)不會遭受與 StreamReader 的這種不兼容,因為它們有一個明確的 — 此時 Read 立即返回值 0 。
TcpClient 和 TcpListener 提供基于任務(wù)的異步方法,以實現(xiàn)可擴展的并發(fā)性。使用這些只是將阻止方法調(diào)用替換為其 *Async 版本并等待返回的任務(wù)的問題。
在下面的示例中,我們編寫了一個異步 TCP 服務(wù)器,該服務(wù)器接受長度為 5,000 字節(jié)的請求,反轉(zhuǎn)字節(jié),然后將其發(fā)送回客戶端:
async void RunServerAsync ()
{
var listener = new TcpListener (IPAddress.Any, 51111);
listener.Start ();
try
{
while (true)
Accept (await listener.AcceptTcpClientAsync ());
}
finally { listener.Stop(); }
}
async Task Accept (TcpClient client)
{
await Task.Yield ();
try
{
using (client)
using (NetworkStream n = client.GetStream ())
{
byte[] data = new byte [5000];
int bytesRead = 0; int chunkSize = 1;
while (bytesRead < data.Length && chunkSize > 0)
bytesRead += chunkSize =
await n.ReadAsync (data, bytesRead, data.Length - bytesRead);
Array.Reverse (data); // Reverse the byte sequence
await n.WriteAsync (data, 0, data.Length);
}
}
catch (Exception ex) { Console.WriteLine (ex.Message); }
}
這樣的程序是可擴展的,因為它不會在請求期間阻塞線程。因此,如果 1,000 個客戶端通過慢速網(wǎng)絡(luò)連接同時連接(例如,每個請求從開始到結(jié)束需要幾秒鐘),則該程序在這段時間內(nèi)不需要 1,000 個線程(與同步解決方案不同)。相反,它僅在 await 表達式之前和之后執(zhí)行代碼所需的短時間內(nèi)租用線程。
.NET 不提供對 POP3 的應(yīng)用程序?qū)又С郑虼四枰?TCP 層寫入才能從 POP3 服務(wù)器接收郵件。幸運的是,這是一個簡單的協(xié)議;POP3 對話是這樣的:
客戶 | 郵件服務(wù)器 | 筆記 |
客戶端連接... | +好的 你好。 | 歡迎辭 |
用戶喬 | +確定 需要密碼。 | |
通票密碼 | +確定已登錄。 | |
列表 | +OK 1 1876 2 5412 3 845 . | 列出服務(wù)器上每封郵件的 ID 和文件大小 |
RETR 1 | +OK 1876 八位字節(jié) . | 檢索具有指定 ID 的郵件 |
德勒 1 | +確定已刪除。 | 從服務(wù)器中刪除郵件 |
退出 | +好的再見。 |
每個命令和響應(yīng)都由換行符 (CR + LF) 終止,但多行 LIST 和 RETR 命令除外,它們由單獨行上的單個點終止。因為我們不能將 StreamReader 與 網(wǎng)絡(luò)流 ,我們可以從編寫一個輔助方法開始,以非緩沖方式讀取一行文本:
string ReadLine (Stream s)
{
List<byte> lineBuffer = new List<byte>();
while (true)
{
int b = s.ReadByte();
if (b == 10 || b < 0) break;
if (b != 13) lineBuffer.Add ((byte)b);
}
return Encoding.UTF8.GetString (lineBuffer.ToArray());
}
我們還需要一個幫助程序方法來發(fā)送命令。因為我們總是期望收到以 +OK 開頭的響應(yīng),所以我們可以同時讀取和驗證響應(yīng):
void SendCommand (Stream stream, string line)
{
byte[] data = Encoding.UTF8.GetBytes (line + "\r\n");
stream.Write (data, 0, data.Length);
string response = ReadLine (stream);
if (!response.StartsWith ("+OK"))
throw new Exception ("POP Error: " + response);
}
編寫這些方法后,檢索郵件的工作很容易。我們在端口 110(默認 POP3 端口)上建立 TCP 連接,然后開始與服務(wù)器通信。在此示例中,我們將每封郵件寫入擴展名為 的隨機命名文件,然后再從服務(wù)器中刪除郵件:
using (TcpClient client = new TcpClient ("mail.isp.com", 110))
using (NetworkStream n = client.GetStream())
{
ReadLine (n); // Read the welcome message.
SendCommand (n, "USER username");
SendCommand (n, "PASS password");
SendCommand (n, "LIST"); // Retrieve message IDs
List<int> messageIDs = new List<int>();
while (true)
{
string line = ReadLine (n); // e.g., "1 1876"
if (line == ".") break;
messageIDs.Add (int.Parse (line.Split (' ')[0] )); // Message ID
}
foreach (int id in messageIDs) // Retrieve each message.
{
SendCommand (n, "RETR " + id);
string randomFile = Guid.NewGuid().ToString() + ".eml";
using (StreamWriter writer = File.CreateText (randomFile))
while (true)
{
string line = ReadLine (n); // Read next line of message.
if (line == ".") break; // Single dot = end of message.
if (line == "..") line = "."; // "Escape out" double dot.
writer.WriteLine (line); // Write to output file.
}
SendCommand (n, "DELE " + id); // Delete message off server.
}
SendCommand (n, "QUIT");
}
可以在 NuGet 上找到開源 POP3 庫,這些庫為協(xié)議方面提供支持,例如身份驗證 TLS/SSL 連接、MIME 分析等。
Windows 憑據(jù)管理器是一個內(nèi)置在 Windows 操作系統(tǒng)中的功能,為用戶提供一種安全的方式來存儲和管理憑據(jù)。本文主要介紹如何在 .NET 中使用可以漫游的 Web 憑據(jù),以及使用中的基本事項。
在前面的文章《試用 Windows Terminal 中的 Terminal Chat 功能》中,我們曾提到為了保證配置的安全,Azure Openai 的配置信息被存儲在憑據(jù)管理器中,類別為Web 憑據(jù)。今天我們就聊一聊如何在 .NET 中如何使用憑據(jù)管理器,本文將主要講述 Web 憑據(jù)的使用。
通過 Windows 憑據(jù)管理器,用戶可以存儲和管理憑據(jù),這些憑據(jù)可以是用戶名和密碼、證書或其他憑據(jù)。我們可以看到憑據(jù)管理器中有兩個類別:Windows 憑據(jù)和 Web 憑據(jù)。
Windows 憑據(jù)管理器的一個特殊功能是它可以安全地存儲用戶的憑據(jù)。這意味著用戶的用戶名和密碼都被加密存儲,以防止未經(jīng)授權(quán)的訪問。此外,憑據(jù)管理器還可以備份和恢復(fù)用戶的憑據(jù),這對于防止憑據(jù)丟失非常有用。
憑據(jù)管理器解決的主要問題是提供了一種簡單而安全的方式來管理用戶的憑據(jù)。用戶無需記住所有的用戶名和密碼,只需要將它們保存在憑據(jù)管理器中,然后在需要時,Windows 就會自動提供這些憑據(jù)。這大大簡化了用戶的工作,同時也提高了安全性。
Web 憑據(jù)是一種特殊類型的憑據(jù),它們用于存儲與 Web 網(wǎng)站和應(yīng)用程序相關(guān)登錄信息的憑據(jù)。Web 憑據(jù)的一個特殊之處在于,它不像 Windows 憑據(jù)那樣可以自行添加和編輯。相反,Web 憑據(jù)是由瀏覽器和應(yīng)用程序自動創(chuàng)建和管理的。
但是 Web 憑據(jù)卻有一個非常棒的地方,那就是它們可以在不同的設(shè)備之間同步。這意味著用戶可以在一個設(shè)備上創(chuàng)建一個 Web 憑據(jù),然后在另一個設(shè)備上使用它。這對于那些經(jīng)常在不同設(shè)備上工作的用戶來說非常有用。
在 UWP 中使用憑據(jù)管理器非常簡單,只需要使用 PasswordVault
類即可。PasswordVault
類提供了一組方法,用于添加、刪除和檢索憑據(jù)。下面我們就來看一下如何使用 PasswordVault
類。以下只是基本的使用方法,更多的使用方法可以參考官方文檔[1]。
添加憑據(jù)
添加憑據(jù)非常簡單,只需要調(diào)用 PasswordVault
類的 Add
方法即可。Add
方法接收一個 PasswordCredential
對象作為參數(shù),PasswordCredential
對象包含了憑據(jù)的詳細信息,包括用戶名、密碼和備注等。
var vault = new PasswordVault();
vault.Add(new PasswordCredential("WebResource", "UserName", "Password"));
刪除憑據(jù)
刪除憑據(jù)也非常簡單,只需要調(diào)用 Remove
方法即可。Remove
方法接收一個 PasswordCredential
對象作為參數(shù),我們可以通過 Retrieve
方法來檢索。
var vault = new PasswordVault();
vault.Remove(vault.Retrieve("WebResource", "UserName"));
當然也可以自己創(chuàng)建一個 PasswordCredential
對象。
var vault = new PasswordVault();
var credential = new PasswordCredential()
{
Resource = "WebResource",
UserName = "UserName",
};
vault.Remove(credential);
檢索憑據(jù)
檢索憑據(jù)前面刪除憑據(jù)中已經(jīng)提到了,這里就不再贅述了。獲取到 PasswordCredential
對象后,我們可以通過 Password
屬性來獲取密碼。
var vault = new PasswordVault();
var password = vault.Retrieve("WebResource", "UserName").Password;
var dialog = new ContentDialog()
{
Title = "Password",
Content = password,
PrimaryButtonText = "OK",
};
await dialog.ShowAsync();
在 WinForm App 和 WPF APP 中使用也是非常簡單,同樣是需要使用 SWindows.Security.Credentials
命名空間中。但是在 WinForm 和 WPF 中,我們在使用 .NET 5 及以上,比如當前的 .NET 8 創(chuàng)建好項目后,需要調(diào)整一下項目的目標框架。改為通過項目的 TargetFramework 屬性指定要訪問的 Windows API 版本。例如:
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
其他就沒有什么區(qū)別了,按照之前的使用方法即可。
新的 WinForm 形式其實也挺好用的,可以使用最新的 .NET 8 進行開發(fā)。非要用 .Net Framework 也不是不行,它也能使用 Window 憑據(jù)管理器,但要通過使用 P/Invoke 來調(diào)用 Windows API。不過,目前還沒有找到使用 web 憑據(jù)的方法。
P/Invoke 是可用于從托管代碼訪問非托管庫中的結(jié)構(gòu)、回調(diào)和函數(shù)的一種技術(shù)。大多數(shù) P/Invoke API 包含在以下兩個命名空間中:System 和 System.Runtime.InteropServices。使用這兩個命名空間可提供用于描述如何與本機組件通信的工具。關(guān)于 P/Invoke 的更多信息可以參考官方文檔[2]。
這里不再贅述 P/Invoke 的使用,感興趣的話可以通過相關(guān)的Win32 API 文檔[3] ,來實現(xiàn)在 .Net Framework 中使用 Windows 憑據(jù)管理器。
在 Windows 平臺使用憑據(jù)管理器雖然方便,但是也有一個需要特別注意的地方:憑據(jù)的安全性。
UWP 應(yīng)用中,擁有天然優(yōu)勢,可以直接使用 PasswordVault
類,并且憑據(jù)顯示有保存者的信息。這樣,就可以防止其他應(yīng)用獲取到憑據(jù)。
如下圖所示:這里面有兩個憑據(jù),雖然他們的Resource
和UserName
屬性都一直,但是一個是顯示了保存者為“CredentialsTest”應(yīng)用,另一個是沒有保存者的。
也就是說,只有保存者為“CredentialsTest”應(yīng)用的才能獲取到憑據(jù)。其他應(yīng)用是無法獲取到憑據(jù)的。
而對于 WinForm 和 WPF 應(yīng)用,雖然也可以使用,但是這種方式就沒有保存者的信息。沒有保存著信息的憑據(jù),就相當于是大家公用的,可以被其他應(yīng)用獲取到憑據(jù)內(nèi)容,并隨意進行刪除和修改,這樣就會存在安全隱患。
如果你是通過桌面應(yīng)用來使用憑據(jù)管理器,那么需要注意憑據(jù)的安全性。因為桌面應(yīng)用是可以被其他應(yīng)用獲取到的。
為了防止惡意應(yīng)用獲取到憑據(jù),當然首先還是建議,能選 UWP 就選 UWP,畢竟 UWP 應(yīng)用的安全性更高。
另外,也可以將憑據(jù)進行加密,然后再保存到憑據(jù)管理器中。這樣即使被獲取到,也無法直接使用。當然,這樣也會帶來一些不便,比如每次使用憑據(jù)時都需要解密,然后再使用。
當然,如果只是使用其漫游和便捷的特性存儲一些不重要的憑據(jù),那么也可以不用考慮安全性問題。
本文主要介紹了如何在.NET環(huán)境下訪問和使用Windows的憑據(jù)管理器,包括在UWP和桌面應(yīng)用中的使用方法。我們還討論了憑據(jù)管理器的安全性問題,以及在使用過程中需要注意的事項。如果你在實踐中遇到任何問題,或者有任何疑問,歡迎留言,我會盡快回復(fù)你。如果你覺得這篇文章對你有幫助,也歡迎分享給你的朋友。
[1]
官方文檔: https://docs.microsoft.com/zh-cn/uwp/api/windows.security.credentials.passwordvault?wt.mc_id=DT-MVP-5005195[2]
官方文檔: https://docs.microsoft.com/zh-cn/dotnet/standard/native-interop/pinvoke?wt.mc_id=DT-MVP-5005195[3]
Win32 API 文檔: https://learn.microsoft.com/zh-cn/windows/win32/api/wincred/?wt.mc_id=DT-MVP-5005195