原文: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 的扩展选项对身份验证过程提供更精细的控制水平。也就是说,您可以对不同的控制器甚至不同的操作方法设置不同的身份验证机制。权衡与主机更好地集成在一起,并较早对不佳的身份验证粒度请求予以拒绝。除了这些常规特性,每个选项都有自己的优缺点,我将在后面的章节中进行介绍。
HTTP 模块这是在 IIS 上运行的一个 Web API 选项。作为 IIS 管道的一部分,HTTP 模块允许较早地执行安全代码。从 HTTP 模块中建立的主体适用于所有的组件,包括管道中稍后运行的 IIS 组件。例如,如果主体是由响应 AuthenticateRequest 事件的 HTTP 模块构建的,则主体的用户名将被正确地记录在 IIS 日志的 cs-username 字段中。HTTP 模块的最大缺点是缺乏粒度。HTTP 模块对进入应用程序的所有请求做出运行反应。对于具有不同功能(如 HTML 标记生成,Web API 等等)的 Web 应用程序,让一个 HTTP 模块以某种方式强制执行身份验证通常不是一个很灵活的方法。在这种情况下,另一个使用 HTTP 模块的缺点就显现出来了,即依赖主机—IIS。
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 请求运行。使用消息处理程序的不足之处在于缺乏更精细的控制。可将消息处理程序配置为对所有请求或对特定路由以全局处理程序来运行。对于给定的路由,您可以有多个控制器。所有这些控制器和它们所包含的操作方法都必须共享相同的由为此路由配置的消息处理程序强制执行的身份验证。换句话说,由消息处理程序执行的身份验证的最低粒度是在路由级别。
操作筛选器由 ASP.NET Web API 提供的另一个扩展选项是操作筛选器。然而,从执行身份验证的角度来看,它不是一个可行的选择,仅仅是因为它在授权筛选器在 ASP.NET Web API 管道运行之后才开始运行。为了让身份验证和授权能正常工作,身份验证必须先于授权而运行。
授权筛选器然而,由 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 未经授权状态代码的请求。它根本没有确定一个经过身份验证的身份,并将如何处理匿名请求的问题留给了授权阶段。身份验证筛选器基本执行三种类型的操作:
- 如果感兴趣的凭据不存在于该请求中,则筛选器就不执行任何操作。
- 如果存在凭据且该凭据是有效的,则筛选器会以经过身份验证的主体的形式确定一个身份。
- 如果存在凭据但该凭据是无效的,筛选器就会通过设置一个错误的结果通知 ASP.NET Web API 框架,这基本上可导致向请求者发回一个“未经授权”的响应。
如果管道中运行的身份验证筛选器都无法检测到无效的凭据,则该管道将继续运行,即使还没有验证未确定的身份。只有根据后来在管道中运行的组件,才能确定如何处理这个匿名请求。
在最基本的层面上,授权筛选器只检查所确定的身份是否是经过身份验证的身份。然而,授权筛选器也可以确保:
- 经过身份验证的身份的用户名在经过允许的用户列表上。
- 至少有一个与经过身份验证的身份相关的角色会列在经过允许的角色列表上。
虽然开箱即用的授权筛选器只根据刚才的描述执行基于角色的访问控制,但是来自开箱即用授权筛选器的自定义授权筛选器却可以通过检查属于由身份验证筛选器确定的身份的声明来执行基于声明的访问控制。
如果所有的授权筛选器都运行正常,管道将继续执行,最终 API 控制器的操作方法会生成一个针对请求的响应。如果未确定身份,或者如果在用户名或角色要求方面存在不匹配,则授权筛选器将拒绝存在 401 未授权响应的请求。图 1 说明了两种筛选器在三种情况下所扮演的角色:不存在凭据、提供的凭据无效和存在的凭据有效。
图 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 扩展点提供对身份验证过程更精细的控制级别。如果您需要对身份验证执行更多的控制,例如,要对不同的控制器,甚至不同的操作方法使用不同的身份验证机制,正确的做法是,通过身份验证筛选器解决身份验证相关问题以及通过授权筛选器解决授权相关问题。