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() { . . . }
}
若要將篩選套用至一個動作,請使用篩選來裝飾動作。 下列程式碼會在 [IdentityBasicAuthentication]
控制器 Post
的 方法上設定篩選。
[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 會在清單中的每一個篩選上呼叫 AuthenticationAsync 。 每個篩選準則都可以驗證要求中的認證。 如果有任何篩選成功驗證認證,篩選準則會建立 IPrincipal 並將它附加至要求。 篩選準則也可以在這個時間點觸發錯誤。 如果是,則管線的其餘部分不會執行。
- 假設沒有任何錯誤,要求會流經管線的其餘部分。
- 最後,Web API 會呼叫每個驗證篩選器的 ChallengeAsync 方法。 篩選準則會使用這個方法,視需要將挑戰新增至回應。 通常 (,但不一定) 回應 401 錯誤。
下圖顯示兩個可能的情況。 在第一個中,驗證篩選器會成功驗證要求、授權篩選器授權要求,而控制器動作會傳回 200 (OK) 。
在第二個範例中,驗證篩選器會驗證要求,但授權篩選準則會傳回 401 (未經授權) 。 在此情況下,不會叫用控制器動作。 驗證篩選器會將Www-Authenticate標頭新增至回應。
其他組合是可行的,例如,如果控制器動作允許匿名要求,您可能會有驗證篩選準則,但沒有授權。
實作 AuthenticateAsync 方法
AuthenticateAsync方法會嘗試驗證要求。 以下是方法簽章:
Task AuthenticateAsync(
HttpAuthenticationContext context,
CancellationToken cancellationToken
)
AuthenticateAsync方法必須執行下列其中一項:
- 無 (無作業) 。
- 建立 IPrincipal ,並在要求上加以設定。
- 設定錯誤結果。
選項 (1) 表示要求沒有篩選所瞭解的任何認證。 選項 (2) 表示篩選已成功驗證要求。 選項 (3) 表示要求具有不正確認證 (,例如錯誤的密碼) ,這會觸發錯誤回應。
以下是實作 AuthenticateAsync的一般大綱。
- 在要求中尋找認證。
- 如果沒有認證,則不執行任何動作,並傳回無作業) (。
- 如果有認證,但篩選無法辨識驗證配置,則不執行任何動作,並傳回 (無作業) 。 管線中的另一個篩選可能會瞭解配置。
- 如果有篩選準則瞭解的認證,請嘗試進行驗證。
- 如果認證不正確,請設定
context.ErrorResult
來傳回 401。 - 如果認證有效,請建立 IPrincipal 並設定
context.Principal
。
下列程式碼會顯示來自基本驗證範例的AuthenticationAsync方法。 批註會指出每個步驟。 此程式碼顯示數種類型的錯誤:沒有認證、格式不正確的認證和錯誤的使用者名稱/密碼的授權標頭。
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);
}
注意:基本驗證範例會將此邏輯放在擴充方法中,以稍微抽象化此邏輯。
結合驗證篩選與Host-Level驗證
「主機層級驗證」是在要求到達 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...
}
}
其他資源
MSDN Magazine ASP.NET Web API安全性篩選 ()