一、设置浏览器客户端

如果你在读这本书,那是因为你理解保护你的 web API 的重要性。 ASP.NET Web API 是一个框架,它帮助构建可以被广泛的客户端利用的 HTTP 服务。 所以确保 Web API 的安全是非常重要的。

ASP.NET Web API 1.0 没有任何安全特性,所以安全是由主机(如 Internet Information Server)提供的。 在 ASP.NET Web API 2,安全特性如 Katana 被引入。 为了保护 Web API,让我们了解所涉及的各种技术,并选择正确的方法。

在本章中,我们将涵盖以下主题:

  • ASP.NET Web API 安全体系结构
  • 设置浏览器客户端
  • 认证和授权
  • 在 HTTP 消息处理程序中实现认证
  • 设置主
  • 使用[Authorize]属性
  • 自定义授权过滤器
  • 控制器操作中的授权

NET Web API 安全体系结构

本节将向您概述 Web API 安全体系结构,并向您展示可用于安全相关事务的所有各种扩展点。 ASP.NET Web API 安全体系结构由三个主要层组成。 宿主层充当 Web API 和网络堆栈之间的接口。 消息处理程序管道层支持实现诸如认证和缓存等横切关注点。 控制器处理层是执行控制器和操作、绑定和验证参数以及创建 HTTP 响应消息的地方。 该层还包含一个过滤器管道,如下图所示:

ASP.NET Web API security architecture

图 1 -此图像显示了保护 Web API 所涉及的组件

让我们简要讨论一下 Web API 管道中每个组件的用途,如下所示:

  • Open Web Interface for . net(OWIN)是新的开放标准托管基础设施。 微软已经在 OWIN 和的基础上构建了自己的框架 Katana,所有的 Web API 安全技术,如认证方法(例如,基于令牌的认证)和对社交登录提供商(例如,谷歌和 Facebook)的支持都将在 OWIN 层实现。
  • 消息处理程序是一个类,用于接收 HTTP 请求并返回 HTTP 响应。 不建议在消息处理程序级别实现认证。 消息处理器用于跨源资源共享(CORS)。
  • 认证过滤器保证在授权过滤器之前运行。 如果您对在 OWIN 层操作认证逻辑不感兴趣,您可以直接转移到控制器或操作。 认证过滤器对于调用基于 owen 的认证逻辑非常有用。
  • 授权过滤器是管道中的位置,您可以在实际昂贵的业务逻辑在模型绑定和验证中运行以及控制器动作被调用之前重新检查请求。

现在我们已经熟悉了安全体系结构,接下来我们将设置客户机。

设置浏览器客户端

让我们创建一个用于联系人查找的 Web API。 这个联系人查找 Web API 服务将向调用的客户端应用返回联系人列表。 然后,我们将使用 jQuery AJAX 调用来列出和搜索联系人,使用 Contact Lookup 服务。

这个应用将帮助我们在本书中演示 Web API 安全性。

实现 Web API 查找服务

在这个节中,我们将创建一个联系人查找 web API 服务,该服务返回一个联系人列表,该列表采用JavaScript 对象标记(JSON)格式。 使用这个联系人查找的客户端是一个简单的 web 页面,它使用 jQuery 显示联系人列表。 按照以下步骤启动项目:

  1. 在 Visual Studio 的Start页面中创建新项目
  2. 选择 Visual c# Installed Template 命名为Web
  3. 选择ASP.NET Web 应用
  4. Name the project ContactLookup and click OK, as shown in the following screenshot:

    Implementing Web API lookup service

    图 2 -我们已经命名了 ASP.NET Web 应用“ContactLookup”

  5. 新 ASP. net 中选择模板。 对话框。

  6. Check Web API and click OK under Add folders and core references, as shown in the following:

    Implementing Web API lookup service

    图 3 -我们选择空 Web API 模板

我们只是创建了一个空的 Web API 项目。 现在让我们添加所需的模型。

添加模型

让我们从开始,通过以下步骤的帮助创建一个代表联系人的简单模型:

  1. First, define a simple contact model by adding a class file to the Models folder.

    Adding a model

    图 4 -右键单击 Models 文件夹并添加一个 Model 类

  2. 命名类文件Contact 并声明Contact类的属性。

    namespace ContactLookup.Models { public class Contact { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public string Mobile { get; set; } } }

我们刚刚添加了一个名为Contact的模型。 现在让我们添加所需的 web API 控制器。

添加控制器

HTTP 请求是由 Web API 中的控制器对象处理的。 让我们定义一个带有两个动作方法的控制器。 一个操作返回联系人列表,另一个操作返回特定于给定 ID 的单个联系人:

  1. Add the Controller under the Controllers folder in Solution Explorer.

    Adding a controller

    图 5 -右键单击 Controllers 文件夹并添加控制器

  2. Select Web API Controller – Empty and click on Add in the Add Scaffold dialog.

    Adding a controller

    图 6 -选择一个空的 Web API 控制器

  3. Let's name the controller ContactsController in the Add Controller dialog box and click Add.

    Adding a controller

    图 7 -命名控制器

    这将在Controllers文件夹中创建ContactsController.cs文件,如下图所示:

    Adding a controller

    图 8 -应用中的 Controllers 文件夹中添加了 ContactsController

  4. 用以下代码替换ContactsController中的代码:

    ``` namespace ContactLookup.Controllers { public class ContactsController : ApiController { Contact[] contacts = new Contact[] { new Contact { Id = 1, Name = "Steve", Email = "steve@gmail.com", Mobile = "+1(234)35434" }, new Contact { Id = 2, Name = "Matt", Email = "matt@gmail.com", Mobile = "+1(234)5654" }, new Contact { Id = 3, Name = "Mark", Email = "mark@gmail.com", Mobile = "+1(234)56789" } };

        public IEnumerable<Contact> GetAllContacts()
        {
            return contacts;
        }
    
        public IHttpActionResult GetContact(int id)
        {
            var contact = contacts.FirstOrDefault(x => x.Id == id);
            if (contact == null)
            {
                return NotFound();
            }
            return Ok(contact);
        }
    }
    

    } ```

为简单起见,联系人存储在控制器类中的固定数组中。 控制器定义为,有两个动作方法。 联系人列表将由GetAllContacts方法以 JSON 格式返回,GetContact方法根据其 ID 返回单个联系人。 一个惟一的 URI 应用于控制器上的每个方法,如下表所示:

|

控制器方法

|

尤里

| | --- | --- | | GetAllContacts | /api/contacts | | GetContact | /api/contacts/id |

使用 JavaScript 和 jQuery 使用 Web API

在这个节中,为了演示使用或不使用任何安全机制调用 web API,让我们创建一个 HTML 页面,使用 web API 并使用 jQuery AJAX 调用的结果更新页面:

  1. In the Solution Explorer pane, right-click on the project and add New Item.

    Consuming the Web API using JavaScript and jQuery

    图 9 -从解决方案资源管理器的上下文菜单中选择 add new item

  2. Create HTML Page named index.html using the Add New Item dialog.

    Consuming the Web API using JavaScript and jQuery

    图 10 -添加索引 html 文件通过选择 html 页面在添加新项目对话框

  3. 用以下代码替换index.html文件中的内容:

    ``` <!DOCTYPE html>

    Contact Lookup

    All Contacts

      Search by ID

      var uri = 'api/contacts';

      $(document).ready(function () {
        // Send an AJAX request
        $.getJSON(uri)
            .done(function (data) {
              // On success, 'data' contains a list of contacts.
              $.each(data, function (key, contact) {
                // Add a list item for the contact.
                $('<li>', { text: formatItem(contact) }).appendTo($('#contacts'));
              });
            });
      });
      

      function formatItem(contact) { return contact.Name + ', email: ' + contact.Email + ', mobile: ' + contact.Mobile; }

      function search() {
        var id = $('#contactId').val();
        $.getJSON(uri + '/' + id)
            .done(function (data) {
              $('#contact').text(formatItem(data));
            })
            .fail(function (jqXHR, textStatus, err) {
              $('#contact').text('Error: ' + err);
            });
      }
      

      ```

    获取联系人列表

    我们需要向/api/contacts 发送一个 HTTP GET 请求来获取联系人列表。 AJAX 请求由 jQuerygetJSON函数发送,并在响应中接收 JSON 对象数组。 如果请求成功,将调用done函数中的回调函数。 在回调中,我们用联系信息更新 DOM,如下所示:

    $(document).ready(function () {
          // Send an AJAX request
          $.getJSON(uri)
              .done(function (data) {
                // On success, 'data' variable contains a list of contacts.
                $.each(data, function (key, contact) {
                  // Add a list item for the contact.
                  $('<li>', { text: formatItem(contact) }).appendTo($('#contacts'));
                });
              });
        });
    

    通过 ID 获取联系人

    要通过 ID 获取联系人,向/api/contacts/id发送一个 HTTP get 请求,其中id是联系人 ID。

    function search() {
          var id = $('#contactId').val();
          $.getJSON(uri + '/' + id)
              .done(function (data) {
                $('#contact').text(formatItem(data));
              })
              .fail(function (jqXHR, textStatus, err) {
                $('#contact').text('Error: ' + err);
              });
        }
    

    getJSON中的请求 URL 有联系人 ID。 响应是此请求的单个联系人的 JSON 表示。

    运行应用

    F5启动调试应用。 输入联系人 ID,点击搜索:

    Running the application

    图 11 -样例基于浏览器的客户端应用的用户界面

    认证授权

    我们创建了一个简单的 web API,它根据 ID 返回联系人列表或特定的联系人。 任何支持 HTTP 的客户端都可以访问这个 web API,但安全性还不够。 在认证和授权机制的帮助下,我们可以保护这个 web API 免受未经授权的访问。

    • 认证机制用于识别合法用户,并使用用户的身份进行认证。 在这里,身份可以是用户名和密码。
    • 授权机制有助于限制对某个动作的未授权访问。 例如,未经授权的用户可以获取联系人列表。 但他只能建立新的联系。

    认证

    认证是在主机Internet Information Service(IIS)中对 web API 进行认证。 Internet 信息服务使用 HTTP 模块进行认证。 我们还可以用自己的 HTTP 模块实现自定义认证。

    主机在对用户进行认证时创建主体。 Principal 是一个IPrincipal对象,它表示运行代码的安全上下文。 您可以从主机连接的Thread.CurrentPrincipal中访问当前的主体。 可以从 principal 的Identity对象访问用户信息。 如果用户通过认证,Identity.IsAuthenticated属性返回true。 如果用户没有被验证,Identity.IsAuthenticated将返回false

    授权

    授权在向控制器提供成功的认证之后发生。 当做出更细粒度的选择时,它帮助您授予对资源的访问权。

    对于任何未经授权的请求,授权筛选器将返回一个错误响应,并且不允许执行该操作。 这是因为授权过滤器将在控制器操作中的任何语句之前首先执行。

    实现 HTTP 消息处理程序中的认证

    对于一个自托管 web API,最佳的实践是在HTTP 消息处理程序中实现认证。 主体将在验证 HTTP 请求后由消息处理程序设置。 对于自托管的 web API,可以考虑在消息处理程序中实现认证。 否则,请使用 HTTP 模块。

    下面的代码片段展示了一个在 HTTP 模块中实现的基本认证示例:

    public class AuthenticationHandler : DelegatingHandler
        {
            protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                                                                   CancellationToken cancellationToken)
            {
                var credentials = ParseAuthorizationHeader(request);
    
                if (credentials != null)
                {
                    // Check if the username and passowrd in credentials are valid against the ASP.NET membership.
                    // If valid, the set the current principal in the request context
                    var identity = new GenericIdentity(credentials.Username);
                    Thread.CurrentPrincipal = new GenericPrincipal(identity, null);;
                }
    
                return base.SendAsync(request, cancellationToken)
                    .ContinueWith(task =>
                    {
                        var response = task.Result;
                        if (credentials == null && response.StatusCode == HttpStatusCode.Unauthorized)
                            Challenge(request, response);
    
                        return response;
                    });
            }
    
            protected virtual Credentials ParseAuthorizationHeader(HttpRequestMessage request)
            {
                string authorizationHeader = null;
                var authorization = request.Headers.Authorization;
                if (authorization != null && authorization.Scheme == "Basic")
                    authorizationHeader = authorization.Parameter;
    
                if (string.IsNullOrEmpty(authorizationHeader))
                    return null;
    
                authorizationHeader = Encoding.Default.GetString(Convert.FromBase64String(authorizationHeader));
    
                var authenticationTokens = authorizationHeader.Split(':');
                if (authenticationTokens.Length < 2)
                    return null;
    
                return new Credentials() { Username = authenticationTokens[0], Password = authenticationTokens[1], };
            }
    
            void Challenge(HttpRequestMessage request, HttpResponseMessage response)
            {
                response.Headers.Add("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", request.RequestUri.DnsSafeHost));
            }
    
            public class Credentials
            {
                public string Username { get; set; }
                public string Password { get; set; }
            }
        }
    

    设置主体

    如果应用实现了自定义认证逻辑,那么我们必须在两个地方设置主体:

    • Thread.CurrentPrincipal是。net 中设置线程主体的标准方法。
    • HttpContext.Current.User是特定于 ASP.NET 的。

    下面的代码显示了如何设置主体:

    private void SetPrincipal(IPrincipal principal)
    {
        Thread.CurrentPrincipal = principal;
        if (HttpContext.Current != null)
        {
            HttpContext.Current.User = principal;
        }
    }
    

    使用[Authorize]属性

    AuthorizeAttribute将确保用户是通过验证还是未通过验证。 如果用户未通过认证,且不调用相应的操作,则返回 HTTP 状态码为 401 的未授权错误。 Web API 允许您以三种方式应用筛选器。 我们可以在全局级别、控制器级别或单个操作级别应用它们。

    全局授权过滤器

    为了对所有 Web API 控制器应用授权过滤器,我们需要将AuthorizeAttribute过滤器添加到Global.asax文件的全局过滤器列表中,如下所示:

    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new AuthorizeAttribute());
    }
    

    控制器级授权过滤器

    要为特定的控制器应用授权过滤器,我们需要用 filter 属性装饰控制器,如下代码所示:

    // Require authorization for all actions on the controller.
    [Authorize]
    public class ContactsController : ApiController
    {
        public IEnumerable<Contact> GetAllContacts() { ... }
        public IHttpActionResult GetContact(int id) { ... }
    }
    

    动作级别授权过滤器

    要对特定的动作应用授权过滤器,我们需要将属性添加到 action 方法中,如下代码所示:

    public class ContactsController : ApiController
    {
        public IEnumerable<Contact> GetAllContacts() { ... }
    
        // Require authorization for a specific action.
        [Authorize]
        public IHttpActionResult GetContact(int id) { ... }
    }
    

    自定义授权过滤器

    要实现自定义授权筛选器,我们需要创建派生AuthorizeAttributeAuthorizationFilterAttributeIAuthorizationFilter的类。

    • AuthorizeAttribute:根据当前用户和用户角色进行授权。
    • AuthorizationFilterAttribute:应用同步授权逻辑,且可能不基于当前用户或角色。
    • IAuthorizationFilter:AuthorizeAttributeAuthorizationFilterAttribute都实现IAuthorizationFilter。 如果需要高级授权逻辑,则需要实现IAuthorizationFilter

    控制器动作中的授权

    有时,可能需要在基于主体处理请求后更改行为。 在这种情况下,我们可以在控制器操作中实现授权。 例如,如果您想根据用户的角色操作响应,我们可以通过动作方法本身的ApiController.User属性验证登录的用户角色:

    public HttpResponseMessage Get()
    {
        if (!User.IsInRole("Admin"))
        {
            // manipulate the response to eliminate information that shouldn't be shared with non admin users
        }
    }
    

    总结

    这很容易,不是吗? 我们刚为 APS 设置了安全系统.NET Web API,我们将在接下来的章节中构建它。

    您学习了 ASP 的安全体系结构.NET Web API,它提供了一个整体的视图。 然后我们设置浏览器客户机,从实现 Web 查找服务到使用 JavaScript 和 jQuery 代码调用 Web API。

    您还学习了认证和授权技术,我们将在本书后面详细介绍这些技术。 接下来,您了解了 HTTP 消息处理程序、Principal 和用于控制用户授权的[Authorize]属性。

    最后,您了解了自定义授权和控制器操作中的授权,以便在基于主体处理请求后更改行为。

    你在这一章学到了很多东西。 然而,这仅仅是个开始。 在下一章中,您将实现 Web API 的安全套接字层。 让我们开始吧!