共用方式為


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 2612RFC 2617 中定義的驗證流程:

  1. 用戶端會在 [授權] 標頭中傳送認證。 這通常會在用戶端收到來自伺服器的 401(未經授權) 回應之後發生。 不過,用戶端可以使用任何要求傳送認證,而不只是在取得 401 之後。
  2. 如果伺服器不接受認證,則會傳回 401 (未經授權) 回應。 回應包含一或多個挑戰的 Www-Authenticate 標頭。 每個挑戰都會指定伺服器所辨識的驗證配置。

伺服器也可以從匿名要求傳回 401。 事實上,這通常是驗證程序起始的方式:

  1. 用戶端會傳送匿名要求。
  2. 伺服器會傳回 401。
  3. 用戶端會以認證重新傳送要求。

此流程同時包含驗證授權步驟。

  • 驗證會證明用戶端的身分識別。
  • 授權決定用戶端是否可以存取特定資源。

在 Web API 中,驗證篩選器會處理驗證,但不會處理授權。 授權應該透過授權篩選器或在控制器動作內完成。

以下是 Web API 2 管線中的流程:

  1. 叫用動作之前,Web API 會建立該動作的驗證篩選器清單。 這包括動作範圍、控制器範圍和全域範圍的篩選器。
  2. Web API 會在清單中的每個篩選器上呼叫 AuthenticateAsync。 每個篩選器都可以驗證要求中的認證。 如果有任何篩選器成功驗證認證,篩選器會建立 IPrincipal 並將它附加至要求。 此時,篩選器也可以觸發錯誤。 如果是,則管線的其餘部分不會執行。
  3. 假設沒有任何錯誤,要求會流經管線的其餘部分。
  4. 最後,Web API 會呼叫每個驗證篩選器的 ChallengeAsync 方法。 篩選器會視需要使用此方法將挑戰新增至回應。 通常 (但不一定) 會在回應 401 錯誤時發生。

下圖顯示兩個可能的情況。 在第一個中,驗證篩選器會成功驗證要求、授權篩選器授權要求,而控制器動作會傳回 200 (確定)。

成功驗證的圖表

在第二個範例中,驗證篩選器會驗證要求,但授權篩選器會傳回 401 (未經授權)。 在這種情況下,不會呼叫控制器動作。 驗證篩選器會將 Www-Authenticate 標頭新增至回應。

未經授權驗證的圖表

其他組合是可能的,例如,如果控制器動作允許匿名要求,您可能會有驗證篩選器,但沒有授權。

實作 AuthenticateAsync 方法

AuthenticateAsync 方法會嘗試驗證要求。 這是方法簽名:

Task AuthenticateAsync(
    HttpAuthenticationContext context,
    CancellationToken cancellationToken
)

AuthenticateAsync 方法必須執行下列其中一項:

  1. 沒有 (無作業)。
  2. 建立 IPrincipal,並在要求上加以設定。
  3. 設定錯誤結果。

選項 (1) 表示要求沒有篩選器了解的任何認證。 選項 (2) 表示篩選已成功驗證要求。 選項 (3) 表示要求具有無效的認證 (例如錯誤的密碼),這會觸發錯誤回應。

以下是實作 AuthenticateAsync 的一般大綱。

  1. 在要求中尋找認證。
  2. 如果沒有認證,則不執行任何動作並傳回 (無作業)。
  3. 如果有認證,但篩選器無法辨識驗證配置,則不執行任何動作並傳回 (無作業)。 管線中的另一個篩選器可能會了解配置。
  4. 如果篩選器了解認證,請嘗試進行驗證。
  5. 如果認證不正確,請藉由設定 context.ErrorResult 傳回 401。
  6. 如果認證有效,請建立 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

ChallengeAsync 的圖表

我將呼叫原始 IHttpActionResult (內部結果),以及新的 IHttpActionResult (外部結果)。 外部結果必須執行以下操作:

  1. 叫用內部結果以建立 HTTP 回應。
  2. 檢查回應。
  3. 視需要將驗證挑戰新增至回應。

下列範例取自基本驗證範例。 它會定義外部結果的 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 屬性會保存內部 IHttpActionResultChallenge 屬性代表 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)