ASP.NET Web API 2 中的驗證篩選器
作者:Mike Wasson
驗證篩選器是驗證 HTTP 要求的元件。 Web API 2 和 MVC 5 都支援驗證篩選器,但它們稍有不同,大部分是在篩選器介面的命名慣例中。 本主題旨在說明 Web API 驗證篩選器。
驗證篩選器可讓您設定個別控制器或動作的驗證配置。 如此一來,您的應用程式就可以針對不同的 HTTP 資源支援不同的驗證機制。
在本文中,我將從 https://github.com/aspnet/samples 的基本驗證範例顯示程式碼。 此範例顯示實作 HTTP 基本存取驗證配置 (RFC 2617) 的驗證篩選器。 篩選器會在名為 IdentityBasicAuthenticationAttribute
的類別中實作。 我不會顯示範例中的所有程式碼,只是說明如何撰寫驗證篩選器的部分。
設定驗證篩選器
與其他篩選器一樣,驗證篩選器可以套用至每個控制器、個別動作,或全域套用至所有 Web API 控制器。
若要將驗證篩選器套用至控制器,請使用篩選器屬性裝飾控制器類別。 下列程式碼會設定控制器類別上的 [IdentityBasicAuthentication]
篩選器,這會為所有控制器的動作啟用基本驗證。
[IdentityBasicAuthentication] // Enable Basic authentication for this controller.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
public IHttpActionResult Post() { . . . }
}
若要將篩選器套用至一個動作,請使用篩選器來裝飾動作。 下列程式碼會設定控制器 Post
方法上的 [IdentityBasicAuthentication]
篩選器。
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
[IdentityBasicAuthentication] // Enable Basic authentication for this action.
public IHttpActionResult Post() { . . . }
}
若要將篩選器套用至所有 Web API 控制器,請將它新增至 GlobalConfiguration.Filters。
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new IdentityBasicAuthenticationAttribute());
// Other configuration code not shown...
}
}
實作 Web API 驗證篩選器
在 Web API 中,驗證篩選器會實作 System.Web.Http.Filters.IAuthenticationFilter 介面。 它們也應該繼承自 System.Attribute,以便套用為屬性。
IAuthenticationFilter 介面有兩種方法:
- 如果存在,AuthenticateAsync 會驗證要求中的認證來驗證要求。
- ChallengeAsync 會視需要將驗證挑戰新增至 HTTP 回應。
這些方法會對應至 RFC 2612 和 RFC 2617 中定義的驗證流程:
- 用戶端會在 [授權] 標頭中傳送認證。 這通常會在用戶端收到來自伺服器的 401(未經授權) 回應之後發生。 不過,用戶端可以使用任何要求傳送認證,而不只是在取得 401 之後。
- 如果伺服器不接受認證,則會傳回 401 (未經授權) 回應。 回應包含一或多個挑戰的 Www-Authenticate 標頭。 每個挑戰都會指定伺服器所辨識的驗證配置。
伺服器也可以從匿名要求傳回 401。 事實上,這通常是驗證程序起始的方式:
- 用戶端會傳送匿名要求。
- 伺服器會傳回 401。
- 用戶端會以認證重新傳送要求。
此流程同時包含驗證和授權步驟。
- 驗證會證明用戶端的身分識別。
- 授權決定用戶端是否可以存取特定資源。
在 Web API 中,驗證篩選器會處理驗證,但不會處理授權。 授權應該透過授權篩選器或在控制器動作內完成。
以下是 Web API 2 管線中的流程:
- 叫用動作之前,Web API 會建立該動作的驗證篩選器清單。 這包括動作範圍、控制器範圍和全域範圍的篩選器。
- Web API 會在清單中的每個篩選器上呼叫 AuthenticateAsync。 每個篩選器都可以驗證要求中的認證。 如果有任何篩選器成功驗證認證,篩選器會建立 IPrincipal 並將它附加至要求。 此時,篩選器也可以觸發錯誤。 如果是,則管線的其餘部分不會執行。
- 假設沒有任何錯誤,要求會流經管線的其餘部分。
- 最後,Web API 會呼叫每個驗證篩選器的 ChallengeAsync 方法。 篩選器會視需要使用此方法將挑戰新增至回應。 通常 (但不一定) 會在回應 401 錯誤時發生。
下圖顯示兩個可能的情況。 在第一個中,驗證篩選器會成功驗證要求、授權篩選器授權要求,而控制器動作會傳回 200 (確定)。
在第二個範例中,驗證篩選器會驗證要求,但授權篩選器會傳回 401 (未經授權)。 在這種情況下,不會呼叫控制器動作。 驗證篩選器會將 Www-Authenticate 標頭新增至回應。
其他組合是可能的,例如,如果控制器動作允許匿名要求,您可能會有驗證篩選器,但沒有授權。
實作 AuthenticateAsync 方法
AuthenticateAsync 方法會嘗試驗證要求。 這是方法簽名:
Task AuthenticateAsync(
HttpAuthenticationContext context,
CancellationToken cancellationToken
)
AuthenticateAsync 方法必須執行下列其中一項:
- 沒有 (無作業)。
- 建立 IPrincipal,並在要求上加以設定。
- 設定錯誤結果。
選項 (1) 表示要求沒有篩選器了解的任何認證。 選項 (2) 表示篩選已成功驗證要求。 選項 (3) 表示要求具有無效的認證 (例如錯誤的密碼),這會觸發錯誤回應。
以下是實作 AuthenticateAsync 的一般大綱。
- 在要求中尋找認證。
- 如果沒有認證,則不執行任何動作並傳回 (無作業)。
- 如果有認證,但篩選器無法辨識驗證配置,則不執行任何動作並傳回 (無作業)。 管線中的另一個篩選器可能會了解配置。
- 如果篩選器了解認證,請嘗試進行驗證。
- 如果認證不正確,請藉由設定
context.ErrorResult
傳回 401。 - 如果認證有效,請建立 IPrincipal 並設定
context.Principal
。
下列程式碼顯示基本驗證範例中的 AuthenticateAsync 方法。 註解會指出每個步驟。 此程式碼顯示多種類型的錯誤:沒有認證、格式錯誤的認證和不正確的使用者名稱/密碼的授權標頭。
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
// 1. Look for credentials in the request.
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// 2. If there are no credentials, do nothing.
if (authorization == null)
{
return;
}
// 3. If there are credentials but the filter does not recognize the
// authentication scheme, do nothing.
if (authorization.Scheme != "Basic")
{
return;
}
// 4. If there are credentials that the filter understands, try to validate them.
// 5. If the credentials are bad, set the error result.
if (String.IsNullOrEmpty(authorization.Parameter))
{
context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
return;
}
Tuple<string, string> userNameAndPassword = ExtractUserNameAndPassword(authorization.Parameter);
if (userNameAndPassword == null)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid credentials", request);
}
string userName = userNameAndPassword.Item1;
string password = userNameAndPassword.Item2;
IPrincipal principal = await AuthenticateAsync(userName, password, cancellationToken);
if (principal == null)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
}
// 6. If the credentials are valid, set principal.
else
{
context.Principal = principal;
}
}
設定錯誤結果
如果認證無效,篩選器必須將 context.ErrorResult
設定為建立錯誤回應的 IHttpActionResult。 如需 IHttpActionResult 的詳細資訊,請參閱 Web API 2 中的動作結果。
基本驗證範例包含適合此用途的 AuthenticationFailureResult
類別。
public class AuthenticationFailureResult : IHttpActionResult
{
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
{
ReasonPhrase = reasonPhrase;
Request = request;
}
public string ReasonPhrase { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}
}
實作 ChallengeAsync
ChallengeAsync 方法的目的是視需要將驗證挑戰新增至回應。 這是方法簽名:
Task ChallengeAsync(
HttpAuthenticationChallengeContext context,
CancellationToken cancellationToken
)
方法是在要求管線中的每個驗證篩選器上進行呼叫。
請務必了解在建立 HTTP 回應之前呼叫 ChallengeAsync,甚至可能在控制器動作執行之前呼叫。 呼叫 ChallengeAsync 時,context.Result
會包含 IHttpActionResult,稍後會用來建立 HTTP 回應。 因此,呼叫 ChallengeAsync 時,您還不知道 HTTP 回應。 ChallengeAsync 方法應該以新的 IHttpActionResult 取代 context.Result
的原始值。 此 IHttpActionResult 必須包裝原始的 context.Result
。
我將呼叫原始 IHttpActionResult (內部結果),以及新的 IHttpActionResult (外部結果)。 外部結果必須執行以下操作:
- 叫用內部結果以建立 HTTP 回應。
- 檢查回應。
- 視需要將驗證挑戰新增至回應。
下列範例取自基本驗證範例。 它會定義外部結果的 IHttpActionResult。
public class AddChallengeOnUnauthorizedResult : IHttpActionResult
{
public AddChallengeOnUnauthorizedResult(AuthenticationHeaderValue challenge, IHttpActionResult innerResult)
{
Challenge = challenge;
InnerResult = innerResult;
}
public AuthenticationHeaderValue Challenge { get; private set; }
public IHttpActionResult InnerResult { get; private set; }
public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage response = await InnerResult.ExecuteAsync(cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
// Only add one challenge per authentication scheme.
if (!response.Headers.WwwAuthenticate.Any((h) => h.Scheme == Challenge.Scheme))
{
response.Headers.WwwAuthenticate.Add(Challenge);
}
}
return response;
}
}
InnerResult
屬性會保存內部 IHttpActionResult。 Challenge
屬性代表 Www-Authentication 標頭。 請注意,ExecuteAsync 會先呼叫 InnerResult.ExecuteAsync
以建立 HTTP 回應,然後視需要新增挑戰。
在新增挑戰之前,請先檢查回應碼。 大部分的驗證配置只會在回應為 401 時新增挑戰,如下所示。 不過,某些驗證配置確實會將挑戰新增至成功回應。 例如,請參閱交涉 (RFC 4559)。
假設 AddChallengeOnUnauthorizedResult
類別,ChallengeAsync 中的實際程式碼很簡單。 您只要建立結果,並將其附加至 context.Result
。
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
var challenge = new AuthenticationHeaderValue("Basic");
context.Result = new AddChallengeOnUnauthorizedResult(challenge, context.Result);
return Task.FromResult(0);
}
注意:基本驗證範例會藉由將邏輯放在擴充功能方法中來抽象化此邏輯。
結合驗證篩選器與主機層級驗證
「主機層級驗證」是在要求到達 Web API 架構之前,由主機執行驗證 (例如 IIS)。
通常,您可能想要為應用程式的其餘部分啟用主機層級驗證,但針對 Web API 控制器停用它。 例如,一般案例是在主機層級啟用表單驗證,但使用 Web API 的權杖型驗證。
若要停用 Web API 管線內的主機層級驗證,請在您的設定中呼叫 config.SuppressHostPrincipal()
。 這會導致 Web API 從任何進入 Web API 管線的要求中移除 IPrincipal。 實際上,它會「取消驗證」要求。
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.SuppressHostPrincipal();
// Other configuration code not shown...
}
}
其他資源
ASP.NET Web API 安全性篩選器 (MSDN Magazine)