构建 ASP.NET Web API RESTful 服务 – 第 8 部分






4.71/5 (5投票s)
这是“构建 ASP.NET Web API RESTful 服务”系列的第八部分。
这是“构建 ASP.NET Web API RESTful 服务”系列的第八部分。我们将涵盖的主题包括:
- 使用 Entity Framework Code First 构建数据库模型 – 第 1 部分
- 为数据访问层应用存储库模式 – 第 2 部分
- 开始使用 ASP.Net Web API - 第 3 部分
- 实现模型工厂、依赖注入和配置格式化程序 - 第 4 部分
- 在 Web API 中实现 HTTP 操作 POST、PUT 和 DELETE - 第 5 部分
- 实现资源关联 - 第 6 部分
- 实现资源分页 - 第 7 部分
- 保护 Web API – 第 8 部分。(本文)
- 为 Web API 版本控制做准备 – 第 9 部分
- 实现版本控制的不同技术 – 第 10 部分
- 使用 CacheCow 和 ETag 缓存资源 - 第 11 部分
更新 (2014 年 3 月 5 日) 增加了两篇涵盖 ASP.NET Web API 2 新功能的新文章
保护 Web API
在本文中,我们将讨论如何保护我们的 eLearning API。到目前为止,客户端到 API 的所有请求都通过 HTTP 协议(http://)进行,通信未加密。但在本文中,我们将在“StudentsController
”中实现身份验证功能,因此我们将发送 Username
和 Password
来验证学生。众所周知,传输机密信息应使用安全的 HTTP (https://)。
强制 Web API 使用 HTTPS
我们可以通过在 IIS 级别配置来强制整个 Web API 使用 HTTPS。但在某些情况下,您可能只想对传输机密信息的特定方法强制使用 HTTPS,而对其他方法使用 HTTP。
为了实现这一点,我们需要使用 Web API 过滤器;过滤器基本上允许我们在控制器方法执行代码之前,在管道中执行一些代码。这个新过滤器将负责检查 URI 方案是否安全,如果不安全,过滤器将拒绝调用并向客户端发送响应,告知他请求应该通过 HTTPS 进行。
我们将添加一个新过滤器,该过滤器派生自 AuthorizationFilterAttribute
。此过滤器包含一个名为 OnAuthorization
的重写方法,在请求未通过 HTTPS 的情况下,我们可以在其中注入一个新响应。
让我们在项目根目录中添加一个名为 _Filters_ 的新文件夹,然后添加一个名为 ForceHttpsAttribute
的新类,该类派生自 System.Web.Http.Filters.AuthorizationFilterAttribute
。
过滤器类的代码如下所示:
public class ForceHttpsAttribute : AuthorizationFilterAttribute
{
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
var request = actionContext.Request;
if (request.RequestUri.Scheme != Uri.UriSchemeHttps)
{
var html = "<p>Https is required</p>";
if (request.Method.Method == "GET")
{
actionContext.Response = request.CreateResponse(HttpStatusCode.Found);
actionContext.Response.Content = new StringContent(html, Encoding.UTF8, "text/html");
UriBuilder httpsNewUri = new UriBuilder(request.RequestUri);
httpsNewUri.Scheme = Uri.UriSchemeHttps;
httpsNewUri.Port = 443;
actionContext.Response.Headers.Location = httpsNewUri.Uri;
}
else
{
actionContext.Response = request.CreateResponse(HttpStatusCode.NotFound);
actionContext.Response.Content = new StringContent(html, Encoding.UTF8, "text/html");
}
}
}
}
通过查看上面的代码,我们使用“actionContext
”参数从请求和响应对象中获取它们。我们在这里做的是检查请求的 URI 方案。因此,如果它不安全 (http://),我们需要在响应正文中返回一个小的 HTML 消息,告知客户端使用https 重新发送请求。
同样,我们将区分 GET
方法和其他方法(POST
、PUT
、DELETE
);因为如果客户端通过 http 发起对现有资源的 GET
请求,我们需要使用 https 方案和 443 SSL 端口重新构建相同的请求,然后将此安全 URI 注入响应的 location 标头。通过这样做,客户端(浏览器)将自动使用 https 方案发起另一个 GET
请求。
对于非 GET
请求,我们将返回 404 状态码(未找到)和一个小的 HTML 消息,告知客户端使用 HTTPS 重新发送请求。
现在,如果我们想将此过滤器强制应用于整个 Web API,我们需要在 WebAPIConfig
类中全局添加此过滤器,代码如下:
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new ForceHttpsAttribute());
}
但是,如果我们想对特定方法或特定控制器强制使用 HTTPS,我们可以添加此过滤器属性 ForceHttps
,如下所示:
//Enforce HTTPS on the entire controller
[Learning.Web.Filters.ForceHttps()]
public class CoursesController : BaseApiController
{
//Enforce HTTPS on POST method only
[Learning.Web.Filters.ForceHttps()]
public HttpResponseMessage Post([FromBody] CourseModel courseModel)
{
}
}
使用基本身份验证对用户进行身份验证
到目前为止,我们 API 中的所有方法都是 public
的,互联网上的任何用户都可以请求任何资源。但在现实场景中,这是不正确的。特定数据应由特定人员访问,因此我们需要对某些资源的请求进行身份验证。在我们示例 API 中,一个很好的客户端身份验证候选者是:
- 向 URI “http://{your_port}/api/students/{userName}” 发起
GET
请求的客户端。这意味着,如果客户端发出请求以获取用户名为“TaiseerJoudeh
”的学生详细信息,则他需要在请求中提供用户名和密码以进行身份验证。同样,我们不会允许已认证用户“TaiseerJoudeh
”获取另一个资源的详细信息,因为他不是资源所有者,他不应该能够查看他的详细信息,例如电子邮件、出生日期、注册的课程等。 - 向 URI “http://{your_port}/api/courses/2/students/{userName}” 发起
POST
请求的客户端。此方法用于将特定学生报名到特定课程。在这里进行身份验证是有意义的,因为如果用户名为“KhaledHassan
”的学生想报名课程 ID 为 2 的课程,则他需要通过提供username
和password
来进行身份验证。否则,任何人都可以将任何学生报名到任何课程。
在我们的场景中,我们将使用基本身份验证来验证请求以上两个资源的用户的身份。为此,我们需要添加一个新的 Web API 过滤器,该过滤器将负责从请求标头读取授权数据,检查身份验证类型是否为“basic
”,验证授权标头中发送的凭据,最后在一切正常的情况下对用户进行身份验证。否则,它将返回状态码为 401(未经授权)的响应,并且不会返回资源。
在深入代码之前,让我们先谈谈基本身份验证。
什么是基本身份验证?
它提供了一种在实际处理 HTTP 请求之前验证请求发送者的方法。这将保护服务器免受拒绝服务攻击 (DoS)。它的工作原理是,发起 HTTP 请求的客户端提供一个 username
和 password
,该组合被 base64 编码,并以 string
的形式放在 HTTP 标头中,格式为“username:password
”。消息的接收者(服务器)应首先验证凭据,并在身份验证成功后才进一步处理请求。
由于 username
和 password
仅被 base64 编码,并且为了避免 password
被泄露给他人,基本身份验证应始终在 SSL 连接 (HTTPS) 上使用。
为了在我们的 API 中应用这一点,让我们添加一个名为 LearningAuthorizeAttribute
的新类,该类派生自 System.Web.Http.Filters.AuthorizationFilterAttribute
。
public class LearningAuthorizeAttribute : AuthorizationFilterAttribute
{
[Inject]
public LearningRepository TheRepository { get; set; }
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
//Case that user is authenticated using forms authentication
//so no need to check header for basic authentication.
if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
{
return;
}
var authHeader = actionContext.Request.Headers.Authorization;
if (authHeader != null)
{
if (authHeader.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase) &&
!String.IsNullOrWhiteSpace(authHeader.Parameter))
{
var credArray = GetCredentials(authHeader);
var userName = credArray[0];
var password = credArray[1];
if (IsResourceOwner(userName, actionContext))
{
//You can use Websecurity or asp.net memebrship provider to login, for
//for he sake of keeping example simple, we used out own login functionality
if (TheRepository.LoginStudent(userName, password))
{
var currentPrincipal =
new GenericPrincipal(new GenericIdentity(userName), null);
Thread.CurrentPrincipal = currentPrincipal;
return;
}
}
}
}
HandleUnauthorizedRequest(actionContext);
}
private string[] GetCredentials(System.Net.Http.Headers.AuthenticationHeaderValue authHeader)
{
//Base 64 encoded string
var rawCred = authHeader.Parameter;
var encoding = Encoding.GetEncoding("iso-8859-1");
var cred = encoding.GetString(Convert.FromBase64String(rawCred));
var credArray = cred.Split(':');
return credArray;
}
private bool IsResourceOwner(string userName,
System.Web.Http.Controllers.HttpActionContext actionContext)
{
var routeData = actionContext.Request.GetRouteData();
var resourceUserName = routeData.Values["userName"] as string;
if (resourceUserName == userName)
{
return true;
}
return false;
}
private void HandleUnauthorizedRequest
(System.Web.Http.Controllers.HttpActionContext actionContext)
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
actionContext.Response.Headers.Add("WWW-Authenticate",
"Basic Scheme='eLearning' location='https://:8323/account/login'");
}
}
在上面的代码中,我们重写了 OnAuthorization
方法并实现了以下内容:
- 从请求标头获取授权数据
- 确保授权标头方案设置为“
basic
”身份验证,并包含 base64 编码的字符串。 - 将 base64 编码的
string
转换为“username:password
”格式的string
,并获取username
和password
。 - 验证身份验证标头中发送的
username
是否与 URI 中的username
相同,以确保只有资源所有者才能查看其详细信息。 - 将凭据与我们的数据库进行验证。
- 如果凭据正确,我们将为当前主体设置身份,因此在后续请求中,用户已被认证。
- 如果凭据不正确,服务器将发送一个 HTTP 响应,状态码为 401(未经授权),并带有 WWW-Authenticate 标头。Web 客户端(浏览器)会处理此响应,向客户端请求用户
ID
和password
。
现在,为了在上面提到的两个方法上应用基本身份验证,我们只需要将“LearningAuthorizeAttribute
”属性添加到这些方法中,代码如下:
public class StudentsController : BaseApiController
{
[LearningAuthorizeAttribute]
public HttpResponseMessage Get(string userName)
{
}
}
public class EnrollmentsController : BaseApiController
{
[LearningAuthorizeAttribute]
public HttpResponseMessage Post(int courseId,
[FromUri]string userName, [FromBody]Enrollment enrollment)
{
}
}
现在,我们需要通过使用 Firefox 和 Fiddler 这两个不同的客户端向 URI:“https://:{your_port}/api/students/TaiseerJoudeh” 发送 GET
请求来测试身份验证。
使用 Firefox 测试用户
我们将通过浏览器发出 get
请求,返回的响应将是 401,因为我们没有提供 username
和 password
,并且会显示“Authentication Required”提示,要求输入 username
和 password
。通过提供正确的凭据,我们将收到状态码 200 以及包含该用户所有特定数据的完整 JSON 对象图。对于同一资源的任何后续请求,代码将不再检查凭据,因为我们已为此用户创建了主体,他已被认证。
使用 Fiddler 测试
使用 Fiddler,我们需要创建包含“username:password
”的 base64 编码 string
,并将其与 authorization 标头一起发送。要生成此 string
,我们将使用 http://www.base64encode.org/,如下面的图片所示:
请注意,这根本不是加密,正如我们之前所说,基本身份验证应仅在 SSL 上使用。
现在,我们将使用编码后的 string
在“Authorization: Basic VGFpc2VlckpvdWRlaDpZRUFSVkZGTw==
”授权标头中传递它。请求将如下图所示:
响应码将是 200 OK,我们将收到该已认证用户的特定数据。
在下一篇文章中,我们将讨论 Web API 的版本控制以及我们如何实现不同的技术来实施版本控制。