WebAPI--安全性,身份验证和授权
在本系列文章中,介绍一些保护未经授权用户的Web API的选项。本系列将涵盖身份验证和授权。
1、身份验证是了解用户的身份。例如,Alice使用其用户名和密码登录,服务器使用密码对Alice进行身份验证。
2、授权决定是否允许用户执行操作。例如,Alice有权获取资源但不能创建资源。
本系列的第一篇文章概述了ASP.NET Web API中的身份验证和授权。其他主题描述了Web API的常见身份验证方案
WebAPI中的身份验证和授权
认证:WebAPI假定身份验证在主机中发生。对于Web托管,主机是IIS,它使用HTTP模块进行身份验证。我们可以将项目配置为使用IIS或ASP.NET中内置的任何身份验证模块,或编写自己的HTTP模块以执行自定义身份验证,当主机对用户进行身份验证时,它会创建一个主体,它是一个IPrincipal对象,表示运行代码的安全上下文。
主机通过设置Thread.CurrentPrincipal将主体附加到当前线程。主体包含关联的Identity对象,其中包含有关用户的信息。如果用户已通过身份验证,则Identity.IsAuthenticated属性将返回true,对于匿名请求,IsAuthenticated返回 false
用于身份验证的HTTP消息处理程序
您可以将身份验证逻辑放入HTTP消息处理程序中,而不是使用主机进行身份验证。在这种情况下,消息处理程序检查HTTP请求并设置主体。
什么时候应该使用消息处理程序进行身份验证 以下是一些权衡:
1、HTTP模块可以查看通过ASP.NET管道的所有请求。消息处理程序仅查看路由到Web API的请求。
2、您可以设置每个路由的消息处理程序,以便将身份验证方案应用于特定路由。
3、HTTP模块特定于IIS。消息处理程序与主机无关,因此它们可以与Web托管和自托管一起使用。
4、HTTP模块参与IIS日志记录,审核等。
5、HTTP模块在管道中运行较早。如果在消息处理程序中处理身份验证,则在处理程序运行之前,不会设置主体。此外,当响应离开消息处理程序时,主体将恢复为先前的主体。
通常,如果您不需要支持自托管,则HTTP模块是更好的选择。如果您需要支持自托管,请考虑使用消息处理程序
设置校长
如果应用程序执行任何自定义身份验证,则必须在两个位置设置主体:
1.Thread.CurrentPrincipal. 此属性是在 .net中设置线程主体的标准方法。
2.HttpContext.Current.User. 此属性特定于ASP.NET。
一下代码显示如何设置主体
private void SetPrincipal(IPrincipal principal){
Thread.CurrentPrincipal= principal;
if(HttpContext.Current!=null){
HttpContext.Current.User=principal;
}
}
对于网络托管,您必须在两个地方设置主体; 否则安全上下文可能会变得不一致。但是,对于自托管,HttpContext.Current为null。因此,为了确保您的代码与主机无关,请在分配给HttpContext.Current之前检查null
授权
授权发生在管道中,靠近控制器。这使您可以在授予资源访问权限时进行更精细的选择。
- 授权过滤器在控制器操作之前运行。如果请求未被授权,则过滤器将返回错误响应,并且不会调用该操作。
- 在控制器操作中,您可以从ApiController.User属性获取当前主体。例如,您可以根据用户名过滤资源列表,仅返回属于该用户的那些资源。
使用【授权】属性
Web API提供了内置的授权过滤器AuthorizeAttribute。此筛选器检查用户是否已通过身份验证。如果不是,则返回HTTP状态代码401(未授权),而不调用该操作。
可以在控制器级别或单个操作级别全局应用筛选器。
全局:要限制每个Web API控制器的访问权限,请将AuthorizeAttribute过滤器添加到全局筛选器列表:
public static void Register(HttpConfiguration config){
config.Filters.Add(new AuthorizeAttribute());
}
控制器:要限制特定控制器的访问,请将过滤器作为属性添加到控制器:
[Authorize]
public class ValuesController:ApiController{
public HttpResponseMessage Get(int id){...}
public HttpResponseMessage Post(){...}
}
操作:要限制对特定操作的访问,请将该属性添加到操作方法:
public class ValuesController:ApiController{
public HttpResponseMessage Get(){...}
// Require authorization for a specific action.
[Authorize]
public HttpResponseMessage Post(){...}
}
或者,您可以使用该[AllowAnonymous]属性限制控制器,然后允许匿名访问特定操作。在以下示例中,该Post方法受到限制,但该Get方法允许匿名访问。
[Authorize]
public class ValuesController : ApiController
{
[AllowAnonymous]
public HttpResponseMessage Get() { ... }
public HttpResponseMessage Post() { ... }
}
在示例中,过滤器允许任何经过身份验证的用户访问受限制的方法; 只有匿名用户被拒。还可以限制对特定用户或特定角色的用户的访问:
// Restrict by user:
[Authorize(Users="Alice,Bob")]
public class ValuesController : ApiController
{
}
// Restrict by role:
[Authorize(Roles="Administrators")]
public class ValuesController : ApiController
{
}
注意 Web API控制器的AuthorizeAttribute过滤器位于System.Web.Http命名空间中。System.Web.Mvc命名空间中有一个类似的MVC控制器过滤器,它与Web API控制器不兼容。
自定义授权过滤器
要编写自定义授权过滤器,请从以下类型之一派生:
- AuthorizeAttribute。扩展此类以基于当前用户和用户角色执行授权逻辑。
- AuthorizationFilterAttribute。扩展此类以执行同步授权逻辑,该逻辑不一定基于当前用户或角色。
- IAuthorizationFilter。实现此接口以执行异步授权逻辑; 例如,如果您的授权逻辑进行异步I / O或网络调用。(如果您的授权逻辑受CPU约束,则从AuthorizationFilterAttribute派生更简单,因为这样您就不需要编写异步方法。)
下图显示了AuthorizeAttribute类的类层次结构。
控制器操作内的授权
在某些情况下,您可能允许继续执行请求,但会根据主体更改行为。例如,您返回的信息可能会根据用户的角色而改变。在控制器方法中,您可以从ApiController.User属性获取当前主体。
Public HttpResponseMessage Get(){
if (User.IsInRole("Administrators"))
{
// ...
} }
在ASP.NET Web API 2.2中使用个人帐户和本地登录保护Web API
本主题说明如何使用OAuth2保护Web API以对成员资格数据库进行身份验证。
在Visual Studio 2013中,Web API项目模板为您提供了三种身份验证选项:
- 个人账户。该应用程序使用会员数据库。
- 组织账户。用户使用Azure Active Directory,Office 365或内部部署Active Directory凭据登录。
- Windows身份验证。此选项适用于Intranet应用程序,并使用Windows身份验证IIS模块。
个人帐户为用户提供了两种登录方式:
- 本地登录。用户在站点注册,输入用户名和密码。该应用程序将密码哈希存储在成员资格数据库中。用户登录时,ASP.NET Identity系统会验证密码。
- 社交登录。用户使用外部服务登录,例如Facebook,Microsoft或Google。应用程序仍在成员资格数据库中为用户创建条目,但不存储任何凭据。用户通过登录外部服务进行身份验证。
使用jQuery发送AJAX请求。我将专注于AJAX调用
1、应用程序在客户端做了什么。
2、服务器上发生了什么。
3、中间的HTTP流量。
首先,我们需要定义一些OAuth2术语。
资源。一些可以保护的数据。
资源服务器。承载资源的服务器。
资源所有者。可以授予访问资源权限的实体。(通常是用户。)
客户端:要访问资源的应用程序。在本文中,客户端是Web浏览器。
访问令牌。授予对资源的访问权限的令牌。
持票人令牌。特定类型的访问令牌,具有任何人都可以使用令牌的属性。换句话说,客户端不需要加密密钥或其他秘密来使用承载令牌。因此,承载令牌只能在HTTPS上使用,并且应该具有相对较短的到期时间。
授权服务器。提供访问令牌的服务器。
应用程序可以充当授权服务器和资源服务器。Web API项目模板遵循此模式。
本地登录凭据流
对于本地登录,Web API使用OAuth2中定义的资源所有者密码流。
1.用户输入名称和密码到客户端。
2.客户端将这些凭据发送到授权服务器。
3.授权服务器验证凭据并返回访问令牌。
4.要访问受保护资源,客户端在HTTP请求的Authorization标头中包含访问令牌。
防止ASP.NET MVC应用程序中的跨站请求伪造(CSRF)攻击
跨站请求伪造(CSRF)是一种攻击,其中恶意站点向用户当前登录的易受攻击的站点发送请求
CSRF攻击的示例:
- 用户www.example.com使用表单身份验证登录。
- 服务器对用户进行身份验证。来自服务器的响应包括身份验证cookie。
- 没有注销,用户访问恶意网站。此恶意网站包含以下HTML表单:
<h1>You Are a Winner!</h1>
<form action="http://example.com/api/account" method="post">
<input type="hidden" name="Transaction" value="withdraw" />
<input type="hidden" name="Amount" value="1000000" />
<input type="submit" value="Click Me"/>
</form>
- 请注意,表单操作会发布到易受攻击的站点,而不是恶意站点。这是CSRF的“跨站点”部分。
- 用户单击“提交”按钮。浏览器包含带有请求的身份验证cookie。
- 请求使用用户的身份验证上下文在服务器上运行,并且可以执行允许经过身份验证的用户执行的任何操作。
虽然此示例要求用户单击表单按钮,但恶意页面可以轻松运行自动提交表单的脚本。此外,使用SSL不会阻止CSRF攻击,因为恶意站点可以发送“https://”请求。
通常,对于使用cookie进行身份验证的网站,CSRF攻击是可能的,因为浏览器会将所有相关的cookie发送到目标网站。但是,CSRF攻击不仅限于利用cookie。例如,Basic和Digest身份验证也很容易受到攻击。用户使用基本或摘要式身份验证登录后。浏览器会自动发送凭据,直到会话结束
防伪令牌
为了帮助防止CSRF攻击,ASP.NET MVC使用反伪造令牌,也称为请求验证令牌。
- 客户端请求包含表单的HTML页面。
- 服务器在响应中包含两个令牌。一个令牌作为cookie发送。另一个放在隐藏的表单字段中。令牌是随机生成的,因此攻击者无法猜测值。
- 当客户端提交表单时,它必须将两个令牌发送回服务器。客户端将cookie令牌作为cookie发送,并将表单令牌发送到表单数据中。(当用户提交表单时,浏览器客户端会自动执行此操作。)
- 如果请求不包含两个令牌,则服务器不允许该请求。
带有隐藏表单标记的HTML表单示例:
<form action="/Home/Test" method="post">
<input name="__RequestVerificationToken" type="hidden"
value="6fGBtLZmVBZ59oUad1Fr33BuPxANKY9q3Srr5y[...]" />
<input type="submit" value="Submit" />
</form>
防伪令牌起作用是因为恶意页面由于同源策略而无法读取用户的令牌。(同源策略阻止托管在两个不同站点上的文档访问彼此的内容。因此在前面的示例中,恶意页面可以向example.com发送请求,但它无法读取响应)
要防止CSRF攻击,请使用防伪令牌和任何身份验证协议,其中浏览器在用户登录后以静默方式发送凭据。这包括基于cookie的身份验证协议,如表单身份验证,以及基本和摘要式身份验证等协议。
对于任何非安全方法(POST,PUT,DELETE),您都应该要求防伪标记。另外,确保安全方法(GET,HEAD)没有任何副作用。此外,如果您启用跨域支持,例如CORS或JSONP,那么即使是安全的方法(如GET)也可能容易受到CSRF攻击,从而允许攻击者读取可能敏感的数据。
ASP.NET MVC中的防伪标记
要将防伪标记添加到Razor页面,请使用HtmlHelper.AntiForgeryToken帮助程序方法:
@using (Html.BeginForm("Manage", "Account")) {
@Html.AntiForgeryToken()
}
此方法添加隐藏的表单字段,并设置cookie令牌。
反CSRF和AJAX
表单令牌可能是AJAX请求的问题,因为AJAX请求可能会发送JSON数据,而不是HTML表单数据。一种解决方案是在自定义HTTP标头中发送令牌。以下代码使用Razor语法生成标记,然后将标记添加到AJAX请求中。通过调用AntiForgery.GetTokens在服务器上生成令牌。
<script>
@functions{
public string TokenHeaderValue()
{
string cookieToken, formToken;
AntiForgery.GetTokens(null, out cookieToken, out formToken);
return cookieToken + ":" + formToken;
}
}
$.ajax("api/values", {
type: "post",
contentType: "application/json",
data: { }, // JSON data goes here
dataType: "json",
headers: {
'RequestVerificationToken': '@TokenHeaderValue()'
}
});
</script>
处理请求时,从请求标头中提取标记。然后调用AntiForgery.Validate方法来验证令牌。该验证,如果令牌是无效的方法将引发异常。
void ValidateRequestHeader(HttpRequestMessage request)
{
string cookieToken = "";
string formToken = "";
IEnumerable<string> tokenHeaders;
if (request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
{
string[] tokens = tokenHeaders.First().Split(':');
if (tokens.Length == 2)
{
cookieToken = tokens[0].Trim();
formToken = tokens[1].Trim();
}
}
AntiForgery.Validate(cookieToken, formToken);
}
ASP.NET Web API 2中的身份验证筛选器
身份验证筛选器是对HTTP请求进行身份验证的组件。Web API 2和MVC 5都支持身份验证过滤器,但它们略有不同,主要在于过滤器接口的命名约定。本主题描述Web API身份验证筛选器。
使用身份验证筛选器可以为各个控制器或操作设置身份验证方案 这样,您的应用可以支持不同HTTP资源的不同身份验证机制。
在本文中,我将在http://aspnet.codeplex.com上显示基本身份验证示例中的代码。该示例显示了一个实现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() { . . . }
}