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 控制器。

若要将身份验证筛选器应用于控制器,请使用 filter 属性修饰控制器类。 以下代码设置控制器类的 [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. 客户端在 Authorization 标头中发送凭据。 这通常在客户端收到来自服务器的 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. 如果没有凭据,则不执行任何操作并返回 (no-op) 。
  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
)

对请求管道中的每个身份验证筛选器调用 方法。

请务必了解 ,ChallengeAsync 是在创建 HTTP 响应 之前 调用的,甚至可能在控制器操作运行之前调用的。 调用 ChallengeAsync 时, context.Result 包含 IHttpActionResult,稍后将使用该 IHttpActionResult 创建 HTTP 响应。 因此,调用 ChallengeAsync 时,你还不知道 HTTP 响应。 ChallengeAsync 方法应将 的原始值context.Result替换为新的 IHttpActionResult。 此 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) 。

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身份验证相结合

“主机级身份验证”是由主机 ((如 IIS) )在请求到达 Web API 框架之前执行的身份验证。

通常,你可能希望为应用程序的其余部分启用主机级身份验证,但对 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 杂志)