ASP.NET Web API 安全筛选器

原文:https://msdn.microsoft.com/zh-cn/magazine/dn781361.aspx浏览器

身份验证和受权是应用程序安全的基础。身份验证经过验证提供的凭据来肯定用户身份,而受权则决定是否容许用户执行请求的操做。安全的 Web API 身份验证基于肯定的身份请求和受权用户请求的资源访问。安全

您能够在 ASP.NET Web API 中使用 ASP.NET Web API 管道中提供的扩展点,以及使用由主机提供的选项来实现身份验证。对于 ASP.NET Web API 的第一个版本,常见的作法是使用受权筛选器或操做筛选器来实现身份验证。ASP.NET Web API 2 引入了一个专门用于此过程的新的身份验证筛选器。这种新的扩展点使身份验证和受权问题被清晰地划分开。在本文中,我会向您介绍这两种安全筛选器,并将身份验证和受权做为 ASP.NET Web API 中独立的两个方面,向您演示如何使用它们来实现将身份验证和受权。服务器

实现安全性方面的选项

经过使用由主机提供的扩展点以及由 ASP.NET Web API 管道本身提供的扩展点可以实现 ASP.NET Web API 中的身份验证和受权。基于主机的选项包括 HTTP 模块和 OWIN 中间件组件,而 ASP.NET Web API 的扩展选项包括消息处理程序、操做筛选器、受权筛选器以及身份验证筛选器。框架

基于主机的选项很好地集成到主机管道中,并能较早拒绝管道中的无效请求。另外一方面,ASP.NET Web API 的扩展选项对身份验证过程提供更精细的控制水平。也就是说,您能够对不一样的控制器甚至不一样的操做方法设置不一样的身份验证机制。权衡与主机更好地集成在一块儿,并较早对不佳的身份验证粒度请求予以拒绝。除了这些常规特性,每一个选项都有本身的优缺点,我将在后面的章节中进行介绍。async

HTTP 模块这是在 IIS 上运行的一个 Web API 选项。做为 IIS 管道的一部分,HTTP 模块容许较早地执行安全代码。从 HTTP 模块中创建的主体适用于全部的组件,包括管道中稍后运行的 IIS 组件。例如,若是主体是由响应 AuthenticateRequest 事件的 HTTP 模块构建的,则主体的用户名将被正确地记录在 IIS 日志的 cs-username 字段中。HTTP 模块的最大缺点是缺少粒度。HTTP 模块对进入应用程序的全部请求作出运行反应。对于具备不一样功能(如 HTML 标记生成,Web API 等等)的 Web 应用程序,让一个 HTTP 模块以某种方式强制执行身份验证一般不是一个很灵活的方法。在这种状况下,另外一个使用 HTTP 模块的缺点就显现出来了,即依赖主机—IIS。ide

OWIN 中间件这是另外一个与主机相关的选项,适用于 OWIN 主机。ASP.NET Web API 2 彻底支持 OWIN。使用 OWIN 中间件确保安全的可能最有说服力的理由是同一中间件能够在不一样的框架中工做。这意味着您能够将多个框架(如 ASP.NET Web API、SignalR 等)用在您的应用程序中,却可使用共同的安全中间件。然而,OWIN 中间件的最小粒度却多是一个缺点,由于 OWIN 中间件在 OWIN 管道中运行而且一般在处理各个请求时被调用。此外,OWIN 中间件只能用于与 OWIN 兼容的主机,虽然这种依赖比起依赖特定的主机/服务器(如 IIS)相对要好些,但这是 HTTP 模块的实际状况。值得注意的一点是,正是因为 Microsoft.Owin.Host.SystemWeb 包,OWIN 中间件才能够在(集成了 IIS 的)ASP.NET 管道中运行。模块化

消息处理程序由 ASP.NET Web API 提供的扩展选项,将使用消息处理程序确保安全的最大好处就是它做为 ASP.NET Web API 框架的概念能够不依赖底层的主机或服务器。此外,消息处理程序仅对 Web API 请求运行。使用消息处理程序的不足之处在于缺少更精细的控制。可将消息处理程序配置为对全部请求或对特定路由以全局处理程序来运行。对于给定的路由,您能够有多个控制器。全部这些控制器和它们所包含的操做方法都必须共享相同的由为此路由配置的消息处理程序强制执行的身份验证。换句话说,由消息处理程序执行的身份验证的最低粒度是在路由级别。ui

操做筛选器由 ASP.NET Web API 提供的另外一个扩展选项是操做筛选器。然而,从执行身份验证的角度来看,它不是一个可行的选择,仅仅是由于它在受权筛选器在 ASP.NET Web API 管道运行以后才开始运行。为了让身份验证和受权能正常工做,身份验证必须先于受权而运行。this

受权筛选器然而,由 ASP.NET Web API 提供的另外一个扩展选项是受权筛选器。对于要求比消息处理程序能提供的更高的粒度的情形,执行自定义身份验证最多见的一种方式是使用受权筛选器。将受权筛选器用于身份验证和受权的主要问题是,ASP.NET Web API 并不保证身份验证筛选器的执行顺序。基本上,这意味着在执行身份验证的受权筛选器运行以前,执行受权的受权筛选器就能够正常运行了,从而使得受权筛选器选项如同操做筛选器选项同样不适合身份验证。编码

身份验证筛选器这是本文的重点所在,它是可用于 ASP.NET Web API 2 的最新扩展选项。身份验证筛选器在消息处理程序以后运行,而且是在其余全部筛选器类型以前运行。所以,它们是实现身份验证相关操做的更好选择。最重要的是,身份验证筛选器是在受权筛选器以前运行的。经过使用专门针对身份验证或受权的筛选器,能够分别处理身份验证和受权相关的问题。

此外,身份验证筛选器提供控制或粒度级别,所以特别有用。以旨在被本机移动应用程序和基于浏览器的 AJAX 应用程序所使用的 Web API 为例。移动应用程序可能会在 HTTP Authorization 标头中显示一个令牌,而 AJAX 应用程序可能将身份验证 Cookie 用做凭据。此外,假设 API 的子集是敏感的,且仅适用于本机移动应用程序,您要确保只能经过提供令牌,而不是提供 Cookie 的方式来访问操做方法(Cookie 很容易受到跨站点请求伪造 [XSRF] 的影响,而在 HTTP Authorization 标头中的令牌则不会)。在这种状况下,身份验证必须以比基于主机的选项,甚至是消息处理程序更精细的粒度级别进行。身份验证筛选器很是适合这个用例。您能够应用基于全部这些控制器的令牌上的身份验证筛选器或必须使用的操做方法,以及基于其余地方的 Cookie 的身份验证筛选器。假设,在这种状况下,您有一些常见的操做方法,想经过令牌或 Cookie 的方式来访问它们。您能够将 Cookie 和令牌身份验证筛选器均应用在这些常见的操做方法上,总会有一个筛选器可以成功进行身份验证。这种控制是能被推上台面的具备最大价值的身份验证筛选器。当须要精确控制身份验证时,正确的作法是,经过身份验证筛选器解决身份验证相关问题以及经过受权筛选器解决受权相关问题。

值得一提的是,开箱即用的身份验证筛选器 (HostAuthenticationFilter) 经过 OWIN 中间件启用了 ASP.NET Web API 身份验证。当 OWIN 身份验证中间件在管道中运行,并试图“主动”身份验证传入的请求时,如须要,也能够将它配置为“被动”身份验证传入的请求。HostAuthenticationFilter 容许依据 Web API 管道中后来的名称运行被动 OWIN 身份验证中间件。这种方法启用了可以在多框架间共享的身份验证代码(包括 Microsoft 提供的 OWIN 身份验证中间件),同时仍容许将每一个操做粒度用于身份验证。

虽然您能够混合使用主机级别的身份验证和基于更细粒度 Web API 管道的身份验证,可是也必须仔细考虑主机级别的身份验证会怎样影响 Web API 身份验证。例如,您可使基于 Cookie 的身份验证中间件处于主机级别,这意味着能够同其余框架配合使用,好比 ASP.NET MVC,可是让 Web API 使用基于 Cookie 的主体会使得它容易受到(好比 XSRF 的)攻击。为了帮助处理这种状况,SuppressDefaultHostAuthentication 扩展方法使 Web API 忽略在主机级别配置的任何身份验证。默认的 Web API Visual Studio 模板在主机级别下启用了 Cookie,并使用在 Web API 级别的承载令牌。由于 Cookie 是在主机级别下启用的,并要求 XSRF 缓解,因此模板还使用 SuppressDefaultHostAuthentication 阻止 Web API 管线使用基于 Cookie 的主体。这样一来,Web API 将只使用基于令牌的主体,您则不须要为 Web API 创建抵御 XSRF 攻击的机制。

使身份验证筛选器和受权筛选器协同工做

在 ASP.NET Web API 管道中,身份验证筛选器第一个运行(紧接着运行的是受权筛选器)的缘由很简单,由于受权取决于肯定的身份,而这正是身份验证的结果。如下为您介绍如何设计身份验证筛选器和受权筛选器以协同工做来保护 ASP.NET Web API。

设计的基本原则是让身份验证筛选器只负责验证凭据,而不是让其处理其余问题。例如,若是未提供凭据,身份验证筛选器将不会拒绝有 401 未经受权状态代码的请求。它根本没有肯定一个通过身份验证的身份,并将如何处理匿名请求的问题留给了受权阶段。身份验证筛选器基本执行三种类型的操做:

  1. 若是感兴趣的凭据不存在于该请求中,则筛选器就不执行任何操做。
  2. 若是存在凭据且该凭据是有效的,则筛选器会以通过身份验证的主体的形式肯定一个身份。
  3. 若是存在凭据但该凭据是无效的,筛选器就会经过设置一个错误的结果通知 ASP.NET Web API 框架,这基本上可致使向请求者发回一个“未经受权”的响应。

若是管道中运行的身份验证筛选器都没法检测到无效的凭据,则该管道将继续运行,即便尚未验证未肯定的身份。只有根据后来在管道中运行的组件,才能肯定如何处理这个匿名请求。

在最基本的层面上,受权筛选器只检查所肯定的身份是不是通过身份验证的身份。然而,受权筛选器也能够确保:

  • 通过身份验证的身份的用户名在通过容许的用户列表上。
  • 至少有一个与通过身份验证的身份相关的角色会列在通过容许的角色列表上。

虽然开箱即用的受权筛选器只根据刚才的描述执行基于角色的访问控制,可是来自开箱即用受权筛选器的自定义受权筛选器却能够经过检查属于由身份验证筛选器肯定的身份的声明来执行基于声明的访问控制。

若是全部的受权筛选器都运行正常,管道将继续执行,最终 API 控制器的操做方法会生成一个针对请求的响应。若是未肯定身份,或者若是在用户名或角色要求方面存在不匹配,则受权筛选器将拒绝存在 401 未受权响应的请求。图 1 说明了两种筛选器在三种状况下所扮演的角色:不存在凭据、提供的凭据无效和存在的凭据有效。

ASP.NET Web API 管道中的安全筛选器 图 1 ASP.NET Web API 管道中的安全筛选器

建立身份验证筛选器

身份验证筛选器是一个实现 IAuthenticationFilter 接口的类。这个接口提供两种方法:AuthenticateAsync 和 ChallengeAsync,以下所示:

public interface IAuthenticationFilter : IFilter
{
  Task AuthenticateAsync(HttpAuthenticationContext context, 
    CancellationToken cancellationToken);
  Task ChallengeAsync(HttpAuthenticationChallengeContext context,
    CancellationToken cancellationToken);
}

AuthenticateAsync 方法接受 HttpAuthenticationContext 做为参数。此上下文就是 AuthenticateAsync 方法将身份验证的结果反馈给 ASP.NET Web API 框架的方式。若是请求消息中包含真实的凭据,传入 HttpAuthenticationContext 对象的 Principal 属性将被设置为通过身份验证的主体。若是凭据无效,HttpAuthenticationContext 参数的 ErrorResult 属性将设置为 UnauthorizedResult。若是该请求消息根本不包含凭据,则 AuthenticateAsync 方法不执行任何操做。图 2 中的代码显示了涵盖这三种状况的 AuthenticateAsync 方法的典型实现。在这个示例中使用的通过身份验证的主体是只有名称和角色声明的 ClaimsPrincipal。

图 2 AuthenticateAsync 方法

public Task AuthenticateAsync(HttpAuthenticationContext context,
  CancellationToken cancellationToken)
{
  var req = context.Request;
  // Get credential from the Authorization header 
  //(if present) and authenticate
  if (req.Headers.Authorization != null &&
    "somescheme".Equals(req.Headers.Authorization.Scheme,
      StringComparison.OrdinalIgnoreCase))
  {
    var creds = req.Headers.Authorization.Parameter;
    if(creds == "opensesame") // Replace with a real check
    {
      var claims = new List<Claim>()
      {
        new Claim(ClaimTypes.Name, "badri"),
        new Claim(ClaimTypes.Role, "admin")
      };
      var id = new ClaimsIdentity(claims, "Token");
      var principal = new ClaimsPrincipal(new[] { id });
      // The request message contains valid credential
      context.Principal = principal;
    }
    else
    {
      // The request message contains invalid credential
      context.ErrorResult = new UnauthorizedResult(
        new AuthenticationHeaderValue[0], context.Request);
    }
  }
  return Task.FromResult(0);
}

您可使用 AuthenticateAsync 方法来实现验证请求中的凭据的核心身份验证逻辑,并使用 ChallengeAsync 方法添加身份验证质询。当状态代码是 401 未经受权时,身份验证质询被加入到响应中,为了检查状态代码,您须要该响应对象。但 ChallengeAsync 方法不容许您检查响应或直接设置质询。事实上,这种方法是在操做方法以前在 Web API 管道的请求处理部分中执行的。然而,ChallengeAsync 方法的参数 HttpAuthenticationChallengeContext 容许将操做结果对象 (IHttpActionResult) 分配给 Result 属性。操做结果对象的 ExecuteAsync 方法等待任务生成响应,检查响应状态代码,并添加 WWW-Authenticate 响应标头。图 3 中的代码显示了 ChallengeAsync 方法的典型实现。在这个示例中,我只添加一个通过硬编码的质询。ResultWithChallenge 类是我建立用来添加质询的操做结果类。

图 3 ChallengeAsync 方法

public Task ChallengeAsync(HttpAuthenticationChallengeContext context,
  CancellationToken cancellationToken)
{
  context.Result = new ResultWithChallenge(context.Result);
  return Task.FromResult(0);
}
public class ResultWithChallenge : IHttpActionResult
{
  private readonly IHttpActionResult next;
  public ResultWithChallenge(IHttpActionResult next)
  {
    this.next = next;
  }
  public async Task<HttpResponseMessage> ExecuteAsync(
    CancellationToken cancellationToken)
  {
    var response = await next.ExecuteAsync(cancellationToken);
    if (response.StatusCode == HttpStatusCode.Unauthorized)
    {
      response.Headers.WwwAuthenticate.Add(
        new AuthenticationHeaderValue("somescheme", "somechallenge"));
    }
    return response;
  }
}

下面的代码显示了完整的筛选器类:

public class TokenAuthenticationAttribute : 
  Attribute, IAuthenticationFilter
{
  public bool AllowMultiple { get { return false; } }
  // The AuthenticateAsync and ChallengeAsync methods go here
}

除了实现 IAuthenticationFilter 接口,从属性中派生能够将此类用做类(控制器)级或方法(操做方法)级的属性。

所以,您能够建立一个只负责身份验证特定凭据(本例中的虚假令牌)的身份验证筛选器。身份验证筛选器没有受权逻辑;它惟一目的是处理身份验证:(在处理请求消息时若是有的话)肯定身份,(在处理响应消息时若是有的话)返回质询。受权筛选器处理受权问题,如检查身份是不是通过身份验证的身份或者已在通过容许的用户或角色列表中列出。

使用受权筛选器

使用受权筛选器的基本目标是执行受权,以肯定用户是否有权访问所请求的资源。Web API 提供了所谓的 AuthorizeAttribute 受权筛选器的使用。应用该筛选器可确保身份是通过身份验证的身份。您还可使用容许的特定用户名和角色列表配置受权属性。图 4 中的代码显示了使用不一样的身份属性来受权,在不一样级别(总的来讲,是控制器级别和操做方法级别)应用的受权筛选器。本示例中的筛选器总体上保证了身份是通过身份验证了的。在控制器级别使用的筛选器保证了身份是通过身份验证了的,而且与该身份相关联的角色中至少有一个是“管理员”。在操做方法级别使用的筛选器确保了身份是通过身份验证的,且用户名是“badri”。这里要注意的一点是,在操做方法级别的受权筛选器也继承了控制器级别和全局级别的筛选器。所以,若要成功完成受权,全部筛选器都必须经过:用户名必须是“badri”,其中一个角色必须是“管理员”,且用户必须通过身份验证。

图 4 使用处于三个不一样级别的受权筛选器

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    // Other Web API configuration code goes here
    config.Filters.Add(new AuthorizeAttribute()); // Global level
  }
}
[Authorize(Roles="admin")] // Controller level
public class EmployeesController : ApiController
{
  [Authorize(Users="badri")] // Action method level
  public string Get(int id)
  {
    return “Hello World”;
  }
}

开箱即用的 AuthorizeAttribute 很是有用,可是若是须要更多的自定义,您能够对其划分子类,来实现其余的受权行为。下面的代码显示了一个自定义的受权筛选器:

public class RequireAdminClaimAttribute : AuthorizeAttribute
{
  protected override bool IsAuthorized(HttpActionContext context)
  {
    var principal =
      context.Request.GetRequestContext().Principal as ClaimsPrincipal;
    return principal.Claims.Any(c => c.Type ==
      "http://yourschema/identity/claims/admin"
      && c.Value == "true");
  }
}

这个筛选器只检查“管理员”自定义声明,但您可使用 HttpActionContext 中的主体和其余附加信息在这里进行自定义受权。

在 ASP.NET Web API 的第一个版本中,自定义受权筛选器常常被误用来实现身份验证,但对于 ASP.NET Web API 2,身份验证筛选器如今管道中有本身的地方,这有助于开发干净的模块化代码,便于分开考虑身份验证和受权方面的问题。

筛选器覆盖

正如我刚才解释的,受权筛选器能够应用在操做方法级别、控制器级别或全局级别。经过全局指定受权筛选器,您能够在全部控制器范围内强制执行对全部操做方法调用的受权。若是您想经过一些方法免于执行全局配置检查,那么使用 AllowAnonymous 属性能够轻松作到这一点。

图 5 中的代码显示了在控制器级别对 AllowAnonymous 属性的使用。虽然受权筛选器在全局范围中应用,但与 PublicResourcesController 一块儿使用的 AllowAnonymous 属性可免于对传入此控制器的请求执行受权。

图 5 使用 AllowAnonymous 属性

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    // Other Web API configuration code goes here
    config.Filters.Add(new AuthorizeAttribute()); // Global level
  }
}
[AllowAnonymous]
public class PublicResourcesController : ApiController
{
  public string Get(int id)
  {
    return “Hello World”;
  }
}

AllowAnonymous 属性提供了一种方式,让特定的操做能够覆盖由更高级别的受权筛选器配置的受权。然而,AllowAnonymous 只容许您覆盖受权。假设您最想要使用 HTTP 基自己份验证对大多数操做进行身份验证,但有一个操做只能用令牌进行身份验证。全局配置令牌身份验证而随后对此操做覆盖身份验证(相似于 AllowAnonymous 覆盖受权的方式)会是不错的方法。

ASP.NET Web API 2 引入了一种新的筛选器类型,以解决这种状况,即覆盖筛选器。不一样于 AllowAnonymous,ASP.NET Web API 2 中引入的覆盖筛选器可与任何类型的筛选器一同工做。覆盖筛选器,顾名思义,能够覆盖在更高级别上配置的筛选器。若要覆盖在更高层次上配置的身份验证筛选器,请使用开箱即用的属性 OverrideAuthentication。若是您有一个适合全局使用的身份验证筛选器,并但愿阻止它运行特定的操做方法或控制器,您能够仅在所需的级别上应用 OverrideAuthentication。

覆盖筛选器的做用远不止于阻止某些筛选器运行。假设您有两种身份验证筛选器,一个用于验证安全令牌,另外一个用于在 HTTP 基本方案中验证用户名/密码。这两种筛选器都在全局范围内应用,使您的 API 足够灵活,能够接受令牌或用户名/密码。下面的代码显示了在全局范围内应用的两种身份验证筛选器:

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    // Other Web API configuration code goes here
    config.Filters.Add(new TokenAuthenticator());
    config.Filters.Add(new HttpBasicAuthenticator(realm: "Magical"));
  }
}

如今,也许您想确保只将令牌用做访问特定操做方法的凭据。OverrideAuthentication 如何使您可以知足这种需求,难道是它禁止全部筛选器运行?下面是覆盖筛选器的重要特征,他们清除了在更高级别上指定的全部筛选器,但不删除与其在同一级别上指定的筛选器。这基本上意味着您能够在某个特定级别上添加一个或多个身份验证筛选器,同时清除在更高级别上的全部其余筛选器。回到仅将令牌用做为访问特定操做方法的凭据的要求,您能够简单地在操做方法级别上指定 OverrideAuthentication 属性和 TokenAuthenticator 属性,以下面的代码所示(这能够确保只有 TokenAuthenticator 为操做方法 GetAllowedForTokenOnly 运行):

public class EmployeesController : ApiController
{
  [OverrideAuthentication] // Removes all authentication filters
  [TokenAuthenticator] // Puts back only the token authenticator
  public string GetAllowedForTokenOnly(int id)
  {
    return “Hello World”;
  }
}

所以,引入了 ASP.NET Web API 2 的覆盖筛选器在全局范围内指定筛选器,并在较低级别上选择性地在必须只对全局行为进行覆盖的领域运行筛选器方面提供了更大的灵活性。

除了 OverrideAuthentication 属性,另外还有开箱即用的称为 OverrideAuthorization 的属性,它能够删除在更高级别上指定的受权筛选器。与 AllowAnonymous 相比,不一样之处在于 OverrideAuthorization 只删除更高级别上的受权筛选器。但不删除与其在相同级别上的指定的受权筛选器。AllowAnonymous 使 ASP.NET Web API 能够跳过相关的受权过程,即便是与 AllowAnonymous 在相同级别上指定的受权筛选器,都将被忽略。

总结

您可使用由主机提供的选项,以及由 ASP.NET Web API 管道提供的扩展点在 ASP.NET Web API 中执行身份验证。基于主机的选项很好地集成到主机管道中,并在早期拒绝管道中的无效请求。ASP.NET Web API 扩展点提供对身份验证过程更精细的控制级别。若是您须要对身份验证执行更多的控制,例如,要对不一样的控制器,甚至不一样的操做方法使用不一样的身份验证机制,正确的作法是,经过身份验证筛选器解决身份验证相关问题以及经过受权筛选器解决受权相关问题。

相关文章
相关标签/搜索