通过


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. 客户端在 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. 没有什么(no-op)。
  2. 创建 IPrincipal 并在请求上设置它。
  3. 设置错误结果。

选项(1)表示请求没有筛选器理解的任何凭据。 选项 (2) 表示筛选器已成功对请求进行身份验证。 选项(3)表示请求的凭据无效(如密码错误),这会触发错误响应。

下面是实现 AuthenticateAsync 的一般大纲。

  1. 在请求中查找凭据。
  2. 如果没有凭据,则不执行任何操作并返回(no-op)。
  3. 如果有凭据,但筛选器无法识别身份验证方案,则不执行任何操作并返回(no-op)。 管道中的另一个筛选器可能了解方案。
  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,稍后用于创建 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 时添加质询,如下所示。 但是,某些身份验证方案确实会向成功响应添加质询。 例如,请参阅 Negotiate (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...
    }
}

其他资源

ASP.NET Web API 安全筛选器 (MSDN 杂志)