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 2612 和 RFC 2617 中定义的身份验证流:
- 客户端在 Authorization 标头中发送凭据。 这通常在客户端收到来自服务器的 401 (未授权) 响应之后发生。 但是,客户端可以使用任何请求发送凭据,而不仅仅是在获取 401 后发送凭据。
- 如果服务器不接受凭据,则返回 401 (未授权) 响应。 响应包含包含一个或多个质询的Www-Authenticate标头。 每个质询指定服务器可识别的身份验证方案。
服务器还可以从匿名请求返回 401。 事实上,身份验证过程通常是这样启动的:
- 客户端发送匿名请求。
- 服务器返回 401。
- 客户端使用凭据重新发送请求。
此流包括 身份验证 和 授权 步骤。
- 身份验证可证明客户端的身份。
- 授权确定客户端是否可以访问特定资源。
在 Web API 中,身份验证筛选器处理身份验证,但不处理授权。 授权应由授权筛选器或控制器操作内部完成。
下面是 Web API 2 管道中的流:
- 在调用操作之前,Web API 会为该操作创建身份验证筛选器列表。 这包括具有操作范围、控制器范围和全局范围的筛选器。
- Web API 对列表中的每个筛选器调用 AuthenticateAsync 。 每个筛选器都可以验证请求中的凭据。 如果任何筛选器成功验证凭据,筛选器将创建 IPrincipal 并将其附加到请求。 此时,筛选器也可能触发错误。 如果是,则管道的其余部分不会运行。
- 假设没有错误,则请求流经管道的其余部分。
- 最后,Web API 调用每个身份验证筛选器的 ChallengeAsync 方法。 如果需要,筛选器使用此方法向响应添加质询。 通常 (但并不总是) 响应 401 错误。
下图显示了两种可能的情况。 在第一个中,身份验证筛选器成功验证请求,授权筛选器授权请求,控制器操作返回 200 (正常) 。
第二个示例中,身份验证筛选器对请求进行身份验证,但授权筛选器返回 401 (未授权) 。 在这种情况下,不会调用控制器操作。 身份验证筛选器将Www-Authenticate标头添加到响应。
其他组合是可能的,例如,如果控制器操作允许匿名请求,则可能具有身份验证筛选器,但没有授权。
实现 AuthenticateAsync 方法
AuthenticateAsync 方法尝试对请求进行身份验证。 下面是方法签名:
Task AuthenticateAsync(
HttpAuthenticationContext context,
CancellationToken cancellationToken
)
AuthenticateAsync 方法必须执行以下操作之一:
- 没有 (无操作) 。
- 创建 IPrincipal 并在请求中设置它。
- 设置错误结果。
选项 (1) 表示请求没有任何筛选器理解的凭据。 选项 (2) 表示筛选器已成功对请求进行身份验证。 选项 (3) 表示请求具有无效凭据 (如错误的密码) ,这会触发错误响应。
下面是实现 AuthenticateAsync 的一般概述。
- 在请求中查找凭据。
- 如果没有凭据,则不执行任何操作并返回 (no-op) 。
- 如果有凭据,但筛选器无法识别身份验证方案,则不执行任何操作并返回 (无操作) 。 管道中的另一个筛选器可能了解方案。
- 如果存在筛选器识别的凭据,请尝试对其进行身份验证。
- 如果凭据不正确,则通过设置
context.ErrorResult
返回 401。 - 如果凭据有效,请创建 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
。
我将原始 IHttpActionResult 称为 内部结果,将新的 IHttpActionResult 称为 外部结果。 外部结果必须执行以下操作:
- 调用内部结果以创建 HTTP 响应。
- 检查响应。
- 根据需要向响应添加身份验证质询。
以下示例取自基本身份验证示例。 它定义外部结果的 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 杂志)