ASP.NET Web API 2 中的驗證篩選Authentication Filters in ASP.NET Web API 2

Mike Wassonby Mike Wasson

驗證篩選器是驗證 HTTP 要求的元件。An authentication filter is a component that authenticates an HTTP request. Web API 2 和 MVC 5 都支援驗證篩選,但它們稍有不同,大部分都是在篩選器介面的命名慣例中。Web API 2 and MVC 5 both support authentication filters, but they differ slightly, mostly in the naming conventions for the filter interface. 本主題描述 Web API 驗證篩選準則。This topic describes Web API authentication filters.

驗證篩選器可讓您設定個別控制器或動作的驗證配置。Authentication filters let you set an authentication scheme for individual controllers or actions. 如此一來,您的應用程式就可以針對不同的 HTTP 資源支援不同的驗證機制。That way, your app can support different authentication mechanisms for different HTTP resources.

在本文中,我將在https://github.com/aspnet/samples上顯示基本驗證範例中的程式碼。In this article, I'll show code from the Basic Authentication sample on https://github.com/aspnet/samples. 此範例顯示的驗證篩選準則會執行 HTTP 基本存取驗證配置(RFC 2617)。The sample shows an authentication filter that implements the HTTP Basic Access Authentication scheme (RFC 2617). 此篩選準則會在名為 IdentityBasicAuthenticationAttribute的類別中執行。The filter is implemented in a class named IdentityBasicAuthenticationAttribute. 我不會顯示範例中的所有程式碼,只是說明如何撰寫驗證篩選器的元件。I won't show all of the code from the sample, just the parts that illustrate how to write an authentication filter.

設定驗證篩選準則Setting an Authentication Filter

就像其他篩選器一樣,驗證篩選器可以套用至每個控制器、每個動作,或全域套用至所有 Web API 控制器。Like other filters, authentication filters can be applied per-controller, per-action, or globally to all Web API controllers.

若要將驗證篩選套用至控制器,請使用 filter 屬性裝飾控制器類別。To apply an authentication filter to a controller, decorate the controller class with the filter attribute. 下列程式碼會在控制器類別上設定 [IdentityBasicAuthentication] 篩選準則,以啟用所有控制器動作的基本驗證。The following code sets the [IdentityBasicAuthentication] filter on a controller class, which enables Basic Authentication for all of the controller's actions.

[IdentityBasicAuthentication] // Enable Basic authentication for this controller.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
    public IHttpActionResult Get() { . . . }
    public IHttpActionResult Post() { . . . }
}

若要將篩選套用至一個動作,請使用篩選準則裝飾動作。To apply the filter to one action, decorate the action with the filter. 下列程式碼會在控制器的 Post 方法上設定 [IdentityBasicAuthentication] 篩選準則。The following code sets the [IdentityBasicAuthentication] filter on the controller's Post method.

[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
    public IHttpActionResult Get() { . . . }

    [IdentityBasicAuthentication] // Enable Basic authentication for this action.
    public IHttpActionResult Post() { . . . }
}

若要將篩選套用至所有 Web API 控制器,請將它新增至GlobalConfigurationTo apply the filter to all Web API controllers, add it to GlobalConfiguration.Filters.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new IdentityBasicAuthenticationAttribute());

        // Other configuration code not shown...
    }
}

執行 Web API 驗證篩選Implementing a Web API Authentication Filter

在 Web API 中,驗證篩選準則會執行IAuthenticationFilter介面。In Web API, authentication filters implement the System.Web.Http.Filters.IAuthenticationFilter interface. 它們也應該繼承自system.object,以便套用為屬性。They should also inherit from System.Attribute, in order to be applied as attributes.

IAuthenticationFilter介面有兩個方法:The IAuthenticationFilter interface has two methods:

  • AuthenticateAsync會驗證要求中的認證(如果有的話)來驗證要求。AuthenticateAsync authenticates the request by validating credentials in the request, if present.
  • ChallengeAsync會視需要將驗證挑戰新增至 HTTP 回應。ChallengeAsync adds an authentication challenge to the HTTP response, if needed.

這些方法對應于rfc 2612rfc 2617中定義的驗證流程:These methods correspond to the authentication flow defined in RFC 2612 and RFC 2617:

  1. 用戶端會在授權標頭中傳送認證。The client sends credentials in the Authorization header. 這通常是在用戶端從伺服器收到401(未經授權)回應之後發生。This typically happens after the client receives a 401 (Unauthorized) response from the server. 不過,用戶端可以使用任何要求來傳送認證,而不只是在取得401之後。However, a client can send credentials with any request, not just after getting a 401.
  2. 如果伺服器不接受認證,則會傳回401(未經授權)的回應。If the server does not accept the credentials, it returns a 401 (Unauthorized) response. 回應包含 Www 驗證標頭,其中包含一或多個挑戰。The response includes a Www-Authenticate header that contains one or more challenges. 每個挑戰都會指定伺服器所能辨識的驗證配置。Each challenge specifies an authentication scheme recognized by the server.

伺服器也可以從匿名要求傳回401。The server can also return 401 from an anonymous request. 事實上,這通常是驗證程式的起始方式:In fact, that's typically how the authentication process is initiated:

  1. 用戶端會傳送匿名要求。The client sends an anonymous request.
  2. 伺服器會傳回401。The server returns 401.
  3. 用戶端會以認證重新傳送要求。The clients resends the request with credentials.

此流程包含驗證授權步驟。This flow includes both authentication and authorization steps.

  • 驗證會證明用戶端的身分識別。Authentication proves the identity of the client.
  • 授權會決定用戶端是否可以存取特定資源。Authorization determines whether the client can access a particular resource.

在 Web API 中,驗證篩選器會處理驗證,而不是授權。In Web API, authentication filters handle authentication, but not authorization. 授權應由授權篩選或在控制器動作內部完成。Authorization should be done by an authorization filter or inside the controller action.

以下是 Web API 2 管線中的流程:Here is the flow in the Web API 2 pipeline:

  1. 在叫用動作之前,Web API 會建立該動作的驗證篩選器清單。Before invoking an action, Web API creates a list of the authentication filters for that action. 這包括具有動作範圍、控制器範圍和全域範圍的篩選準則。This includes filters with action scope, controller scope, and global scope.
  2. Web API 會針對清單中的每個篩選呼叫AuthenticateAsyncWeb API calls AuthenticateAsync on every filter in the list. 每個篩選器都可以驗證要求中的認證。Each filter can validate credentials in the request. 如果有任何篩選成功驗證認證,篩選準則會建立IPrincipal ,並將它附加至要求。If any filter successfully validates credentials, the filter creates an IPrincipal and attaches it to the request. 篩選準則也會在此時觸發錯誤。A filter can also trigger an error at this point. 若是如此,管線的其餘部分就不會執行。If so, the rest of the pipeline does not run.
  3. 假設沒有發生錯誤,要求會流經管線的其餘部分。Assuming there is no error, the request flows through the rest of the pipeline.
  4. 最後,Web API 會呼叫每個驗證篩選器的ChallengeAsync方法。Finally, Web API calls every authentication filter's ChallengeAsync method. 篩選器會使用此方法,視需要將挑戰新增至回應。Filters use this method to add a challenge to the response, if needed. 通常(但不一定)會發生以回應401錯誤。Typically (but not always) that would happen in response to a 401 error.

下圖顯示兩個可能的案例。The following diagrams show two possible cases. 在第一種情況下,驗證篩選器會成功驗證要求,授權篩選準則會授權要求,而控制器動作會傳回200(確定)。In the first, the authentication filter successfully authenticates the request, an authorization filter authorizes the request, and the controller action returns 200 (OK).

在第二個範例中,驗證篩選器會驗證要求,但授權篩選會傳回401(未經授權)。In the second example, the authentication filter authenticates the request, but the authorization filter returns 401 (Unauthorized). 在此情況下,不會叫用控制器動作。In this case, the controller action is not invoked. 驗證篩選準則會將 Www 驗證標頭新增至回應。The authentication filter adds a Www-Authenticate header to the response.

其他組合也可能—例如,如果控制器動作允許匿名要求,您可能會有驗證篩選準則,但沒有授權。Other combinations are possible—for example, if the controller action allows anonymous requests, you might have an authentication filter but no authorization.

執行 AuthenticateAsync 方法Implementing the AuthenticateAsync Method

AuthenticateAsync方法會嘗試驗證要求。The AuthenticateAsync method tries to authenticate the request. 以下是方法簽章:Here is the method signature:

Task AuthenticateAsync(
    HttpAuthenticationContext context,
    CancellationToken cancellationToken
)

AuthenticateAsync方法必須執行下列其中一項動作:The AuthenticateAsync method must do one of the following:

  1. 無(無 op)。Nothing (no-op).
  2. 建立IPrincipal ,並在要求上設定它。Create an IPrincipal and set it on the request.
  3. 設定錯誤結果。Set an error result.

選項(1)表示要求沒有篩選器所瞭解的任何認證。Option (1) means the request did not have any credentials that the filter understands. 選項(2)表示篩選已成功驗證要求。Option (2) means the filter successfully authenticated the request. 選項(3)表示要求具有不正確認證(例如錯誤的密碼),這會觸發錯誤回應。Option (3) means the request had invalid credentials (like the wrong password), which triggers an error response.

以下是執行AuthenticateAsync的一般大綱。Here is a general outline for implementing AuthenticateAsync.

  1. 在要求中尋找認證。Look for credentials in the request.
  2. 如果沒有任何認證,則不執行任何動作,也不會傳回(無 op)。If there are no credentials, do nothing and return (no-op).
  3. 如果有認證,但篩選準則無法辨識驗證配置,則不執行任何動作,也不會傳回(無 op)。If there are credentials but the filter does not recognize the authentication scheme, do nothing and return (no-op). 管線中的另一個篩選器可能會瞭解配置。Another filter in the pipeline might understand the scheme.
  4. 如果有篩選器瞭解的認證,請嘗試進行驗證。If there are credentials that the filter understands, try to authenticate them.
  5. 如果認證不正確,請設定 context.ErrorResult以傳回401。If the credentials are bad, return 401 by setting context.ErrorResult.
  6. 如果認證有效,請建立IPrincipal ,並設定 context.PrincipalIf the credentials are valid, create an IPrincipal and set context.Principal.

下列程式碼顯示基本驗證範例中的AuthenticateAsync方法。The follow code shows the AuthenticateAsync method from the Basic Authentication sample. 批註會指出每個步驟。The comments indicate each step. 程式碼會顯示數種類型的錯誤:沒有認證的授權標頭、認證格式不正確,以及使用者名稱/密碼不正確。The code shows several types of error: An Authorization header with no credentials, malformed credentials, and bad username/password.

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;
    }

}

設定錯誤結果Setting an Error Result

如果認證無效,則篩選準則必須將 context.ErrorResult 設定為會建立錯誤回應的應傳回 iHTTPactionresultIf the credentials are invalid, the filter must set context.ErrorResult to an IHttpActionResult that creates an error response. 如需應傳回 iHTTPactionresult的詳細資訊,請參閱Web API 2 中的動作結果For more information about IHttpActionResult, see Action Results in Web API 2.

基本驗證範例包含適用于此用途的 AuthenticationFailureResult 類別。The Basic Authentication sample includes an AuthenticationFailureResult class that is suitable for this purpose.

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;
    }
}

執行 ChallengeAsyncImplementing ChallengeAsync

ChallengeAsync方法的目的是要在需要時,將驗證挑戰新增至回應。The purpose of the ChallengeAsync method is to add authentication challenges to the response, if needed. 以下是方法簽章:Here is the method signature:

Task ChallengeAsync(
    HttpAuthenticationChallengeContext context,
    CancellationToken cancellationToken
)

方法是在要求管線中的每個驗證篩選準則上呼叫。The method is called on every authentication filter in the request pipeline.

請務必瞭解, ChallengeAsync是在建立 HTTP 回應之前呼叫,甚至可能在控制器動作執行之前進行。It's important to understand that ChallengeAsync is called before the HTTP response is created, and possibly even before the controller action runs. 呼叫ChallengeAsync時,context.Result 包含應傳回 iHTTPactionresult,稍後用來建立 HTTP 回應。When ChallengeAsync is called, context.Result contains an IHttpActionResult, which is used later to create the HTTP response. 因此,在呼叫ChallengeAsync時,您還不知道 HTTP 回應的任何內容。So when ChallengeAsync is called, you don't know anything about the HTTP response yet. ChallengeAsync方法應該以新的應傳回 iHTTPactionresult取代 context.Result 的原始值。The ChallengeAsync method should replace the original value of context.Result with a new IHttpActionResult. 應傳回 iHTTPactionresult必須包裝原始 context.ResultThis IHttpActionResult must wrap the original context.Result.

我會呼叫內部結果的原始應傳回 iHTTPactionresult ,而新的會應傳回 iHTTPactionresult 外部結果I'll call the original IHttpActionResult the inner result, and the new IHttpActionResult the outer result. 外部結果必須執行下列動作:The outer result must do the following:

  1. 叫用內部結果以建立 HTTP 回應。Invoke the inner result to create the HTTP response.
  2. 檢查回應。Examine the response.
  3. 如有需要,請將驗證挑戰新增至回應。Add an authentication challenge to the response, if needed.

下列範例取自基本驗證範例。The following example is taken from the Basic Authentication sample. 它會定義外部結果的應傳回 iHTTPactionresultIt defines an IHttpActionResult for the outer result.

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 屬性會保存內部應傳回 iHTTPactionresultThe InnerResult property holds the inner IHttpActionResult. Challenge 屬性代表 Www 驗證標頭。The Challenge property represents a Www-Authentication header. 請注意, ExecuteAsync會先呼叫 InnerResult.ExecuteAsync 來建立 HTTP 回應,然後視需要新增挑戰。Notice that ExecuteAsync first calls InnerResult.ExecuteAsync to create the HTTP response, and then adds the challenge if needed.

請檢查回應碼,然後再新增挑戰。Check the response code before adding the challenge. 如果回應為401,大部分的驗證配置只會新增挑戰,如下所示。Most authentication schemes only add a challenge if the response is 401, as shown here. 不過,某些驗證配置會對成功回應新增挑戰。However, some authentication schemes do add a challenge to a success response. 例如,請參閱Negotiate (RFC 4559)。For example, see Negotiate (RFC 4559).

假設有 AddChallengeOnUnauthorizedResult 類別,則ChallengeAsync中的實際程式碼很簡單。Given the AddChallengeOnUnauthorizedResult class, the actual code in ChallengeAsync is simple. 您只需建立結果,並將其附加至 context.ResultYou just create the result and attach it to 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);
}

注意:基本驗證範例會將此邏輯一併放在擴充方法中,藉此將它抽象化。Note: The Basic Authentication sample abstracts this logic a bit, by placing it in an extension method.

將驗證篩選器與主機層級驗證結合Combining Authentication Filters with Host-Level Authentication

「主機層級驗證」是由主機(例如 IIS)執行的驗證,在要求到達 Web API 架構之前。"Host-level authentication" is authentication performed by the host (such as IIS), before the request reaches the Web API framework.

通常,您可能會想要為應用程式的其餘部分啟用主機層級驗證,但請將它停用於您的 Web API 控制器。Often, you may want to enable host-level authentication for the rest of your application, but disable it for your Web API controllers. 例如,典型的案例是在主機層級啟用表單驗證,但針對 Web API 使用權杖型驗證。For example, a typical scenario is to enable Forms Authentication at the host level, but use token-based authentication for Web API.

若要在 Web API 管線內停用主機層級驗證,請在您的設定中呼叫 config.SuppressHostPrincipal()To disable host-level authentication inside the Web API pipeline, call config.SuppressHostPrincipal() in your configuration. 這會導致 Web API 從輸入 Web API 管線的任何要求中移除IPrincipalThis causes Web API to remove the IPrincipal from any request that enters the Web API pipeline. 實際上,它 "取消驗證要求"。Effectively, it "un-authenticates" the request.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.SuppressHostPrincipal();

        // Other configuration code not shown...
    }
}

其他資源Additional Resources

ASP.NET Web API 安全性篩選(MSDN 雜誌)ASP.NET Web API Security Filters (MSDN Magazine)