在 ASP.NET Web API 2.2 中使用個別帳戶和本機登入保護 Web API
作者:Mike Wasson
本主題旨在示範如何使用 OAuth2 保護 Web API,以針對成員資格資料庫進行驗證。
教學課程中使用的軟體版本
在 Visual Studio 2013 中,Web API 專案範本提供三個選項供您進行驗證:
- 個人帳戶。 應用程式會使用成員資格資料庫。
- 組織帳戶。 使用者使用其 Azure Active Directory、Office 365 或內部部署 Active Directory 認證登入。
- Windows 驗證。 這個選項適用於內部網路應用程式,並使用 Windows 驗證 IIS 模組。
個別帳戶提供兩種方式讓使用者登入:
- 本機登入。 使用者會在網站註冊,輸入使用者名稱和密碼。 應用程式會將密碼雜湊儲存在成員資格資料庫中。 當使用者登入時,ASP.NET 身分識別系統會驗證密碼。
- 社群登入。 使用者使用外部服務登入,例如 Facebook、Microsoft 或 Google。 應用程式仍會為成員資格資料庫中的使用者建立專案,但不會儲存任何認證。 使用者會登入外部服務來進行驗證。
本文將探討本機登入案例。 針對本機和社群登入,Web API 會使用 OAuth2 來驗證要求。 不過,本機和社群登入的認證流程不同。
在本文中,我將示範簡單的應用程式,讓使用者能夠登入並將已驗證的 AJAX 呼叫傳送至 Web API。 您可以在這裡下載範例程式碼。 讀我檔案會說明如何在 Visual Studio 中從頭開始建立範例。
範例應用程式會針對資料繫結使用 Knockout.js,而 jQuery 則用於傳送 AJAX 要求。 我將專注於 AJAX 呼叫,因此您不需要知道本文的 Knockout.js。
在此過程中,我會說明:
- 應用程式在用戶端上執行的動作。
- 伺服器上發生的情況。
- 中間的 HTTP 流量。
首先,我們需要定義一些 OAuth2 術語。
- 資源。 可以保護的一些資料片段。
- 資源伺服器。 託管資源的伺服器。
- 資源擁有者。 可授與存取資源授權的實體。 (通常是使用者。)
- 用戶端:想要存取資源的應用程式。 在本文中,用戶端是網頁瀏覽器。
- 存取權杖。 授與資源存取權的權杖。
- 持有人權杖。 特定類型的存取權杖,具有任何人都可以使用權杖的屬性。 換句話說,用戶端不需要密碼編譯金鑰或其他祕密,才能使用持有人權杖。 因此,持有人權杖只能透過 HTTPS 使用,而且應該有相對較短的到期時間。
- 授權伺服器。 提供存取權杖的伺服器。
應用程式可以同時作為授權伺服器和資源伺服器。 Web API 專案範本會遵循此模式。
本機登入認證流程
針對本機登入,Web API 會使用 OAuth2 中定義的資源擁有者密碼流程。
- 使用者會將名稱和密碼輸入用戶端。
- 用戶端將這些認證傳送到授權伺服器。
- 授權伺服器會驗證認證並傳回存取權杖。
- 若要存取受保護的資源,用戶端會在 HTTP 要求的授權標頭中包含存取權杖。
當您在 Web API 專案範本中選取 [個別帳戶] 時,專案會包含驗證使用者認證和發行權杖的授權伺服器。 下圖顯示 Web API 元件中的相同認證流程。
在此案例中,Web API 控制器會作為資源伺服器。 驗證篩選器會驗證存取權杖,並使用 [授權] 屬性來保護資源。 當控制器或動作具有 [授權] 屬性時,必須驗證該控制器或動作的所有要求。 否則會拒絕授權,而 Web API 會傳回 401 (未經授權) 錯誤。
授權伺服器和驗證篩選器都會呼叫處理 OAuth2 詳細資料的 OWIN 中介軟體元件。 稍後在本教學課程中,我會更詳細地描述設計。
傳送未經授權的要求
若要開始使用,請執行應用程式,然後按一下 [呼叫 API] 按鈕。 要求完成時,您應該會在 [結果] 方塊中看到錯誤訊息。 這是因為要求不包含存取權杖,因此要求未經授權。
[呼叫 API] 按鈕會將 AJAX 要求傳送至 ~/api/values,以叫用 Web API 控制器動作。 以下是傳送 AJAX 要求的 JavaScript 程式碼區段。 在範例應用程式中,所有 JavaScript 應用程式程式碼都位於 Scripts\app.js 檔案中。
// If we already have a bearer token, set the Authorization header.
var token = sessionStorage.getItem(tokenKey);
var headers = {};
if (token) {
headers.Authorization = 'Bearer ' + token;
}
$.ajax({
type: 'GET',
url: 'api/values/1',
headers: headers
}).done(function (data) {
self.result(data);
}).fail(showError);
在使用者登入之前,沒有持有人權杖,因此要求中沒有 Authorization 標頭。 這會導致要求傳回 401 錯誤。
以下是 HTTP 要求。 (我使用了用來擷取 HTTP 流量的 Fiddler。)
GET https://localhost:44305/api/values HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Accept-Language: en-US,en;q=0.5
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
HTTP 回應:
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
WWW-Authenticate: Bearer
Date: Tue, 30 Sep 2014 21:54:43 GMT
Content-Length: 61
{"Message":"Authorization has been denied for this request."}
請注意,回應包含 Www-Authenticate 標頭,並將挑戰設定為 Bearer。 這表示伺服器需要持有人權杖。
註冊使用者
在應用程式的 [註冊] 區段中,輸入電子郵件和密碼,然後按一下 [註冊] 按鈕。
您不需要為此範例使用有效的電子郵件位址,但實際的應用程式會確認位址。 (請參閱透過登入、電子郵件確認和密碼重設建立安全 ASP.NET MVC 5 Web 應用程式。) 針對密碼,請使用類似「Password1」,其中包含大寫字母、小寫字母、數字和非英數字元。 為了讓應用程式保持簡單,我排除了用戶端驗證,因此,如果密碼格式有問題,您會收到 400 (不正確的要求) 錯誤。
[註冊] 按鈕會將 POST 要求傳送到 ~/api/Account/Register/。 要求內文是保留名稱和密碼的 JSON 物件。 以下是傳送要求的 JavaScript 程式碼:
var data = {
Email: self.registerEmail(),
Password: self.registerPassword(),
ConfirmPassword: self.registerPassword2()
};
$.ajax({
type: 'POST',
url: '/api/Account/Register',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(data)
}).done(function (data) {
self.result("Done!");
}).fail(showError);
HTTP 要求,其中 $CREDENTIAL_PLACEHOLDER$
是密碼機碼/值組的預留位置:
POST https://localhost:44305/api/Account/Register HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 84
{"Email":"alice@example.com",$CREDENTIAL_PLACEHOLDER1$,$CREDENTIAL_PLACEHOLDER2$"}
HTTP 回應:
HTTP/1.1 200 OK
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 00:57:58 GMT
Content-Length: 0
這個要求是由 AccountController
類別處理。 在內部,AccountController
使用 ASP.NET 身分識別來管理成員資格資料庫。
如果您從 Visual Studio 本機執行應用程式,使用者帳戶會儲存在 LocalDB 的 AspNetUsers 資料表中。 若要在 Visual Studio 中檢視資料表,請按一下 [檢視] 功能表、選取 [伺服器總管],然後展開 [資料連線]。
取得存取權杖
到目前為止,我們沒有完成任何 OAuth,但現在我們將會在要求存取權杖時看到 OAuth 授權伺服器運作。 在範例應用程式的 [登入] 區域中,輸入電子郵件和密碼,然後按一下 [登入]。
[登入] 按鈕會將要求傳送至權杖端點。 要求的本文包含下列表單 URL 編碼的資料:
- grant_type: "password"
- username:<使用者的電子郵件>
- password:<密碼>
以下是傳送 AJAX 要求的 JavaScript 程式碼:
var loginData = {
grant_type: 'password',
username: self.loginEmail(),
password: self.loginPassword()
};
$.ajax({
type: 'POST',
url: '/Token',
data: loginData
}).done(function (data) {
self.user(data.userName);
// Cache the access token in session storage.
sessionStorage.setItem(tokenKey, data.access_token);
}).fail(showError);
如果要求成功,授權伺服器會在回應主體中傳回存取權杖。 請注意,我們會將權杖儲存在工作階段存放區中,以便稍後在將要求傳送至 API 時使用。 不同於某些形式的驗證 (例如 Cookie 型驗證),瀏覽器不會在後續的要求中自動包含存取權杖。 應用程式必須明確執行此動作。 這是件好事,因為它會限制 CSRF 弱點。
HTTP 要求:
POST https://localhost:44305/Token HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 68
grant_type=password&username=alice%40example.com&password=Password1!
您可以看到要求包含使用者的認證。 您必須使用 HTTPS 來提供傳輸層安全性。
HTTP 回應:
HTTP/1.1 200 OK
Content-Length: 669
Content-Type: application/json;charset=UTF-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:22:36 GMT
{
"access_token":"imSXTs2OqSrGWzsFQhIXziFCO3rF...",
"token_type":"bearer",
"expires_in":1209599,
"userName":"alice@example.com",
".issued":"Wed, 01 Oct 2014 01:22:33 GMT",
".expires":"Wed, 15 Oct 2014 01:22:33 GMT"
}
為了方便閱讀,我縮排 JSON 並截斷存取權杖,這需要相當長的時間。
access_token
、token_type
和 expires_in
屬性是由 OAuth2 規格所定義。其他屬性 (userName
、.issued
和 .expires
) 僅供參考使用。 您可以在 /Providers/ApplicationOAuthProvider.cs 檔案中找到在 TokenEndpoint
方法中新增這些額外屬性的程式碼。
傳送已驗證的要求
既然我們有持有人權杖,我們可以對 API 提出已驗證的要求。 這是藉由在要求中設定 [授權] 標頭來完成。 再次按一下 [呼叫 API] 按鈕以查看此項目。
HTTP 要求:
GET https://localhost:44305/api/values/1 HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Authorization: Bearer imSXTs2OqSrGWzsFQhIXziFCO3rF...
X-Requested-With: XMLHttpRequest
HTTP 回應:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:41:29 GMT
Content-Length: 27
"Hello, alice@example.com."
登出
因為瀏覽器不會快取認證或存取權杖,所以登出只是將權杖從工作階段存放區中移除,以「忘記」權杖:
self.logout = function () {
sessionStorage.removeItem(tokenKey)
}
了解個別帳戶專案範本
當您在 ASP.NET Web 應用程式專案樣本中選取 [個別帳戶] 時,專案包括:
- OAuth2 授權伺服器。
- 用於管理使用者帳戶的 Web API 端點
- 用於儲存使用者帳戶的 EF 模型。
以下是實作這些功能的主要應用程式類別:
AccountController
. 提供管理使用者帳戶的 Web API 端點。 此Register
動作是唯一在本教學課程中使用的動作。 類別上的其他方法支援密碼重設、社群登入和其他功能。- /Models/IdentityModels.cs 中定義的
ApplicationUser
。 這個類別是成員資格資料庫中使用者帳戶的 EF 模型。 - /App_Start/IdentityConfig.cs 中定義的
ApplicationUserManager
。此類別衍生自 UserManager,並在使用者帳戶上執行作業,例如建立新的使用者、驗證密碼等等,並自動保存資料庫的變更。 ApplicationOAuthProvider
. 此物件會插入 OWIN 中介軟體,並處理中介軟體所引發的事件。 其衍生自 OAuthAuthorizationServerProvider。
設定授權伺服器
在 StartupAuth.cs 中,下列程式碼會設定 OAuth2 授權伺服器。
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// Note: Remove the following line before you deploy to production:
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
TokenEndpointPath
屬性是授權伺服器端點的 URL 路徑。 這是應用程式用來取得持有人權杖的 URL。
Provider
屬性會指定插入 OWIN 中介軟體的提供者,並處理中介軟體所引發的事件。
以下是應用程式想要取得權杖的基本流程:
- 若要取得存取權杖,應用程式會將要求傳送至 ~/Token。
- OAuth 中介軟體會在提供者上呼叫
GrantResourceOwnerCredentials
。 - 提供者會呼叫
ApplicationUserManager
來驗證認證,並建立宣告身分識別。 - 如果成功,提供者會建立驗證票證,用來產生權杖。
OAuth 中介軟體不了解有關使用者帳戶的任何資訊。 提供者會在中介軟體與 ASP.NET 身分識別之間進行通訊。 如需實作授權伺服器的詳細資訊,請參閱 OWIN OAuth 2.0 授權伺服器。
設定 Web API 以使用持有人權杖
在 WebApiConfig.Register
方法中,下列程式碼會設定 Web API 管線的驗證:
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
HostAuthenticationFilter 類別會使用持有人權杖來啟用驗證。
SuppressDefaultHostAuthentication 方法會指示 Web API 忽略在請求到達 Web API 管道之前通過 IIS 或 OWIN 中介軟體進行的任何身份驗證。 如此一來,我們可以限制 Web API 只驗證使用持有人權杖的要求。
注意
特別是,應用程式的 MVC 部分可能會使用表單驗證,以將認證儲存在 Cookie 中。 Cookie 型驗證需要使用防偽權杖,以防止 CSRF 攻擊。 這是 Web API 的問題,因為 Web API 無法便利地將防偽權杖傳送給用戶端。 (如需此問題的更多背景,請參閱防止 Web API 中的 CSRF 攻擊。呼叫 SuppressDefaultHostAuthentication 可確保 Web API 不會容易受到儲存在 Cookie 中認證的 CSRF 攻擊。
當用戶端要求受保護的資源時,以下是 Web API 管線中會發生的情況:
- HostAuthentication 篩選器會呼叫 OAuth 中介軟體來驗證權杖。
- 中介軟體會將權杖轉換成宣告身分識別。
- 此時,要求已驗證,但未獲得授權。
- 授權篩選器會檢查宣告身分識別。 如果宣告授權該資源的使用者,則會授權要求。 根據預設,[授權] 屬性會授權任何已驗證的要求。 不過,您可以透過角色或其他宣告來授權。 如需詳細資訊,請參閱 Web API 中的驗證和授權。
- 如果先前的步驟成功,控制器會傳回受保護的資源。 否則,用戶端會收到 401 (未經授權) 錯誤。
其他資源
- ASP.NET Identity
- 了解 VS2013 RC 的 SPA 範本中的安全性功能。 Hongye Sun 的 MSDN 部落格文章。
- 剖析 Web API 個別帳戶範本 – 第 2 部分:本機帳戶。 Dominick Baier 的部落格文章。
- 使用 OWIN 主機驗證和 Web API。 Brock Allen 所提供的
SuppressDefaultHostAuthentication
和HostAuthenticationFilter
完善說明。 - 自訂 VS 2013 範本的 ASP.NET 身分識別中的設定檔資訊。 Pranav Rastogi 的 MSDN 部落格文章。
- ASP.NET Identity 中 UserManager 類別的每個要求存留期管理。 Suhas Joshi 的 MSDN 部落格文章,
UserManager
對類別有很完善的說明。