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 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 會在清單中的每一個篩選上呼叫 AuthenticationAsync 。 每個篩選準則都可以驗證要求中的認證。 如果有任何篩選成功驗證認證,篩選準則會建立 IPrincipal 並將它附加至要求。 篩選準則也可以在這個時間點觸發錯誤。 如果是,則管線的其餘部分不會執行。
  3. 假設沒有任何錯誤,要求會流經管線的其餘部分。
  4. 最後,Web API 會呼叫每個驗證篩選器的 ChallengeAsync 方法。 篩選準則會使用這個方法,視需要將挑戰新增至回應。 通常 (,但不一定) 回應 401 錯誤。

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

成功驗證的圖表

在第二個範例中,驗證篩選器會驗證要求,但授權篩選準則會傳回 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

下列程式碼會顯示來自基本驗證範例的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

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 會保存內部 IHttpActionResult。 屬性 Challenge 代表Www-Authentication標頭。 請注意, ExecuteAsync 會先呼叫 InnerResult.ExecuteAsync 以建立 HTTP 回應,然後視需要新增挑戰。

新增挑戰之前,請先檢查回應碼。 大部分的驗證配置只會在回應為 401 時新增挑戰,如下所示。 不過,某些驗證配置確實會將挑戰新增至成功回應。 例如,請參閱 交涉 (RFC 4559) 。

假設類別, AddChallengeOnUnauthorizedResultChallengeAsync 中的實際程式碼很簡單。 您只要建立結果,並將其附加至 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安全性篩選 ()