三、使用 ASP.NET 网络应用编程接口的服务层

本章将通过示例介绍 ASP.NET 网络应用编程接口,并为我们的电影票应用创建服务层结构。如果您不是. NET 程序员,或者您不想弄乱数据和服务层,请随意跳过这一章。我们在本章中构建的服务是在线提供的,可以在构建示例应用时使用。该服务实例可以用作后端,同时遵循电影票应用集成。本章并不是对 ASP.NET 网络应用编程接口的完整参考,而是详细介绍了它的特性以及如何使用它来为我们的移动客户端开发、服务后端。需要了解 ASP.NET MVC,但不必遵循本章的内容。

在本章中,我们将介绍:

  • 创建网络应用编程接口服务
  • 选择途径
  • 参数绑定
  • 为电影票应用构建服务
  • 内容协商
  • 保护网络应用编程接口

ASP.NET Web API(原名 WCF Web API)是一个通过 HTTP 轻松构建面向资源服务的框架,可以使用 HTTP 的全部功能,可以被包括浏览器、控制台应用、服务、移动设备等在内的各种各样的客户端消费。它是在上构建 RESTful 应用的理想平台.NET 框架。网络应用编程接口是作为 ASP.NET MVC 4 的一部分提供的,它可以在 Visual Studio 2012 中获得,也可以作为 Visual Studio 2010 Service Pack 1 的附加组件。

ASP.NET 网络应用编程接口将 HTTP 作为应用级协议,利用 HTTP 的概念,并提供以下特性:

  • 控制器中内置的动作映射,基于 HTTP 动词(GET、POST、PUT 等)
  • 内容协商:使用该功能,服务根据客户端的请求决定响应的格式
  • 请求/响应主体可以由任何类型的内容组成,例如二进制文件(图像/音频/视频...),HTML,JSON 等等;而不仅仅是 SOAP 所要求的 XML 内容
  • 主机独立:既可以在 ASP.NET 内部托管,也可以在我们自己的主机进程中自托管

创建网络应用编程接口服务

现在让我们打开 Visual Studio 2012,用 C#为我们的电影票应用创建 ASP.NET 网络应用编程接口后端服务。

请注意,我们在本章中构建的服务层可通过以下网址在线获得:

http://API . kenomoble book . com/API/

如果您不想在本地创建服务,这里提供的服务方法可以用于测试和与您的应用集成。

现在,执行以下步骤来创建网络应用编程接口服务:

  1. Make sure that you have installed Visual Studio 2012/2010 with MVC 4. If not installed, you can use the Microsoft Web Platform Installer to install them from the following link:

    http://www.microsoft.com/web/downloads/platform.aspx

  2. Launch Visual Studio, create a new project by navigating to the File menu and then New | Project.... Now from the pop up menu, select Visual C# Template from the left menu and select the ASP.NET MVC 4 Web Application template. Name the project as MovieTickets.WebAPI and click on OK.

    Creating a Web API service

  3. In the project pop-up window, as shown in the following, select the Web API template from the template list. If you are interested in writing unit tests for your Web API services, you can check the Create a unit test project checkbox and Visual Studio will automatically create a test project for you.

    您可以从列表中选择任何一个模板,因为它们都将包含创建网络应用编程接口控制器所需的动态链接库文件和配置。我们选择了网络T2 应用编程接口模板,因为这是唯一带有样本网络应用编程接口控制器的模板。

    Creating a Web API service

  4. 我们的 ASP.NET 网络应用编程接口服务现在由 Visual Studio 生成,带有大量与 MVC 4 相关的文件夹(内容图像脚本视图等),这些文件夹可以从项目中删除,因为我们将仅将此解决方案视为 ASP.NET 网络应用编程接口服务。

控制器文件夹中,生成两个控制器:HomeController.csValuesController.cs。第一个控制器名叫HomeController.cs,是一个 MVC 4 控制器,ValuesController.cs是一个 Web API 控制器。与其他 MVC 4 文件和文件夹一起,HomeController.cs文件也可以从项目中移除。我们目前唯一感兴趣的文件是ValuesController.cs,它包含ValuesController类,是一个 Web API 控制器,继承自基类ApiController,不像 MVC 控制器是从Controller基类派生出来的。

Creating a Web API service

如果我们通过点击打开ValuesController.cs,我们可以看到 Visual Studio 已经生成了与 HTTP 动词GETPOSTPUTDELETE相匹配的方法,它们是 HTTP 请求执行的典型操作。

使用 IIS 托管

现在我们已经创建了我们的服务,让我们看看它是如何运行的,是使用 ASP.NET 运行时托管的。在本地 IIS 中我们的服务所在的文件夹上创建一个应用(如果您安装了 IIS Express ,您也可以使用它),以便可以使用您机器上的以下网址访问该服务:

http://localhost/movietickets.webapi

根据计算机上 IIS 的版本,执行此操作的步骤可能会有所不同。在 IIS 7 上,您可以使用以下步骤配置新应用:

  1. 通过在 Visual Studio 中转到构建 | 构建解决方案来构建解决方案。
  2. 点击窗口启动按钮,在运行文本框中,输入inetmgr并点击进入
  3. 打开 IIS,右键点击默认网站
  4. Select Add Application... as shown in the following screenshot:

    Hosting using IIS

  5. 别名字段中输入MovieTickets.WebAPI

  6. In the Physical path field, provide the physical path of the root directory of the project we created earlier.

    Hosting using IIS

  7. 点击确定,我们的 Web API 服务就可以使用了。

要测试是否一切正常,打开你最喜欢的浏览器,点击地址栏中的以下网址:

http://localhost/movietickets.webapi/api/values

浏览器上会显示一个带有数据value1value2的 XML 文件,数据是通过ValuesController文件中的GET方法返回的,如下图截图所示:

Hosting using IIS

类型

不要忘记在网址中添加/api/,否则会出现 404 错误。这里api只是作为避免同一个项目中 MVC 和 Web API 控制器之间路由冲突的一种便捷方式。您可以在下一节看到这是如何工作的。

路由

路由、简单来说就是接收请求并将其映射到控制器动作的过程。如果你熟悉 MVC 路由,你会发现 Web API 路由和它类似,但有一些区别;Web API 使用 HTTP 请求类型而不是 URL 中的动作名称(如 MVC 的情况)来选择要在控制器中执行的动作。

网络应用编程接口的路由配置在文件App_Start\WebApiConfig.cs中定义。ASP.NET 网络应用编程接口的默认路由配置如下所示:

namespace MovieTickets.WebAPI
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.Routes.MapHttpRoute(
                 name: "DefaultApi",
 routeTemplate: "api/{controller}/{id}",
                 defaults: new { id = RouteParameter.Optional }
             );
        }
    }
}

Web API 的默认路由模板是api/{controller}/{id},如前面代码中突出显示的。

如果我们在文件App_Start\RouteConfig.cs中打开 MVC 的默认路由配置,我们可以看到默认路由定义是作为{controller}/{action}/{id}提供的。

namespace MovieTickets.WebAPI
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new
                {
                    controller = "Home",
                    action = "Index",
                    id = UrlParameter.Optional
                }
            );
        }
    }
}

两种路由定义可以注意到的一个明显区别是,在 Web API 路由定义中,缺少{action}路由值。这是因为在网络应用编程接口中,HTTP 动词用于定位动作,因此 URI 不需要提供动作名称。

例如,当网络应用编程接口接收到对特定控制器的GET请求时,匹配动作的列表都是名称以GET开头的方法(例如,GetMovies、get 加热器等)。所有其他 HTTP 动词也是如此。通过匹配方法的签名来识别要调用的正确操作方法。

可以通过使用动作方法的HttpGetHttpPutHttpPostHttpDelete属性来覆盖动作方法的命名约定。例如,我们可以将动作方法Get(int id)重命名为MyValues(int id),并用HttpGet属性进行修饰:

[HttpGet]
public string MyValues(int id)
{
    return "my value";
}

现在我们用 URL http://localhost/movietickets.webapi/api/value/1打浏览器,可以看到字符串my value会被返回。如果属性HttpGet从动作中移除,网络应用编程接口将不会将方法MyValues识别为动作方法。

定制网络应用编程接口路由

默认的网络应用编程接口路由适用于理想的场景,在这种场景中,每个 HTTP 动词只定义了几个动作方法,它们只在方法签名上有所不同。根据您的项目需求,您最终可能需要多个GET方法或多个POST方法,这些方法具有不同的功能和名称,调用这些方法的应用需要通过它们的名称来调用它们,而不是允许网络应用编程接口使用 HTTP 动词来计算方法名称。

让我们通过在我们的ValuesController中添加另一个名为GetMyValues的动作方法来看看这个动作场景:

public string GetMyValues()
{
    return "Get my values";
}

现在的要求是,我们需要调用这个特定的操作方法,让我们通过使用这个 URL 调用这个方法来尝试这样做:

http://localhost/movietickets.webapi/api/values/GetMyValues

结果将是一条错误消息,如下图所示:

Customizing the Web API routing

消息称找到了多个与请求匹配的操作:,这是因为网络应用编程接口路由识别了两个与该请求匹配的操作方法;GetGetMyValues

为了解决这个问题,我们需要在WebApiConfig类中添加以下路由配置,该类在当前配置之上使用基于动作名称的路由:

public static void Register(HttpConfiguration config)
        {
            config.Routes.MapHttpRoute(
                name: "DefaultApi2",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
  }

确保您将此路由配置添加到了Web API 默认配置之上,否则默认路由将优先,并且会发现多个操作...错误将再次显示。

一旦添加了前面的代码,我们点击了网址

http://localhost/movietickets.webapi/api/values/Getmyvalues再次在浏览器上,我们可以看到新的结果:

Customizing the Web API routing

参数绑定

参数绑定 就是将一个 HTTP 请求中的内容转换为.NET 对象,以便可以向操作方法参数提供值。如果没有参数绑定支持,开发人员将不得不在操作方法中编写大量容易出错且乏味的代码,以从原始 HTTP 请求中检索参数值。

在 URI,输入参数通常作为查询字符串嵌入到 HTTP 请求中,或者嵌入到请求的正文中。Web API 使用名为模型绑定 的技术从查询字符串中读取参数值,并使用格式化程序 从请求体中读取。

模型绑定

如果你熟悉 ASP.NET MVC,那么你会发现它和网络应用编程接口中使用的模型绑定概念是一样的,用于从用application/form-url-encoded编码的查询字符串、标题或正文中读取值。当遇到请求时,在ValueProviderFactories类中注册的值提供者从请求中提取数据,并将它们提供给模型绑定器,模型绑定器使用这些值创建模型对象。

默认情况下,简单类型如s tringDate TimeGuiddecimal和其他用类型转换器 装饰的类型使用模型绑定。

[ModelBinder]属性可用于参数或参数类型,以强制模型绑定。源自[ModelBinder][FromUri]属性告诉模型绑定器只在 URI 寻找值。

格式化程序

网络应用编程接口使用格式化程序从参数绑定的 HTTP 请求体中读取数据。网络应用编程接口中引入了格式化程序,以提供更好的内容协商能力。内容协商是一种机制,它允许 web 服务器根据客户端的请求,使用相同的 URL 以不同的格式提供内容,这将在本章后面讨论。

媒体格式化程序源自MediaTypeFormatter类。所有配置的格式化程序都可以在HttpConfiguration.Formatters集合中找到。网络应用编程接口使用传入请求的内容类型从该集合中标识要使用的正确格式化程序。

ASP.NET 网络应用编程接口中的请求正文内容被视为只读、一次读取和非缓冲流。这意味着:

  • 我们在动作方法签名中只能有一个complex类型。如果需要一个以上的complex类型,除了一个以外的所有其他类型都必须使用[ModelBinder]属性进行装饰。
  • 如果格式化程序用于从请求体中读取参数值,那么在操作方法中将无法再访问请求体。

这与 MVC 的参数绑定有很大的不同,在 MVC 中,请求体被缓冲,以便可以多次搜索参数值。

网络应用编程接口为 XML 和 JSON 提供现成的格式化程序,开发人员可以创建自定义格式化程序来支持其他媒体类型。

类型

我们可以使用[FromBody]属性来指定参数应该从正文中读取,而不是从 URI 中读取。该属性在简单类型作为POST请求的参数发送的情况下非常有用。默认情况下,网络应用编程接口将在 URI 寻找一个simple类型,由于请求是一个POST,该值将在请求的正文中。

ValuesController中,Post动作方法有【FromBody]属性的实现:

public void Post([FromBody]string value)
      {
      }

为电影票应用构建服务

现在我们已经介绍了 ASP.NET 网络应用编程接口的一些基础知识,让我们开始为我们的电影票应用构建后端服务。由于这本书不是关于数据库或访问数据的,我们遵循的方法是对服务在存储库类中返回的数据进行硬编码(对象的数据是在实例化对象时添加的),而不是从数据库中读取数据。

让我们来看一下我们服务架构的高级视图:

Building a service for the Movie Tickets application

服务架构被简化了,这样我们就不会花很多时间在书的上下文之外的主题上,比如数据库设计、数据访问等等。

来自客户端移动应用的请求将由 ASP.NET 网络应用接口请求管道接收,并将调用控制器中的一个操作方法。动作方法将依次调用业务层 获取或更新数据,业务层依次调用存储库类 返回硬编码数据 。在本章中,我们将只讨论创建一个控制器和操作方法。示例应用和所有其他控制器和方法可以在 Packt 网站上的代码包中找到,文件夹名为chapter 3。这种方法有助于避免没有新信息的冗余代码,并帮助我们专注于书中的重要主题。

按照以下步骤设置电影列表的数据,这些数据将显示在我们的“电影票”应用的主页上:

  1. BLL\BusinessObjects文件夹中创建一个名为MovieBO.cs的文件,并创建一个名为MovieBO的类,如下代码所示:

    cs public class MovieBO { public int MovieId { get; set; } public string Name { get; set; } public string Rating { get; set; } public string Length { get; set; } public string Genre { get; set; } public string LeadStars { get; set; } public string Image { get; set; } public bool IsNowPlaying { get; set; } }

  2. Create a class called MovieRepository in the file BLL\ MovieRepository.cs, which is responsible for generating data which our service will return to the client. After adding the new class, add a class-level public static variable called moviesMasterList:

    ```cs public static List moviesMasterList;

    ```

    这个变量将保存一个电影的主列表,业务逻辑将查询这个集合来选择匹配它的查询。

  3. 现在,让我们添加一个名为CreateMoviesMasterList 的方法,为moviesMasterList变量添加几部电影:

    cs private static void CreateMoviesMasterList() { moviesMasterList = new List<MovieBO>() { new MovieBO(){ MovieId = 1, Name = "The Great Gatsby (2013)", Genre = "Drama", Image = "http://images.kendomobilebook.com/movies/greatgatsby.jpg", IsNowPlaying = true, LeadStars = "Leonardo DiCaprio, Amitabh Bachchan", Length = "143", Rating = "PG-13" }, new MovieBO(){ MovieId = 2, Name= "Iron man 3", Genre= "Sci-Fi", Image = "http://images.kendomobilebook.com/movies/ironman3.jpg", IsNowPlaying = true, LeadStars = "Robert Downey Jr., Gwyneth Paltrow", Length = "130", Rating = "PG-13" } }; }

  4. 现在创建一个名为GetMoviesMasterList 的公共静态方法来返回电影主列表:

    cs public static List<MovieBO> GetMoviesMasterList() { if (moviesMasterList == null || moviesMasterList.Count == 0) { CreateMoviesMasterList(); } return moviesMasterList; }

  5. 为了分层架构的,让我们创建一个业务层,它只不过是BLL\ MovieTicketsBLL.cs文件中一个名为MovieTicketsBLL的类,它将包含访问存储库并将业务对象返回给控制器的静态方法。让我们添加一个名为GetMovies的方法,它将根据提供的搜索字符串返回电影。如果搜索字符串为空,则返回整个电影列表:

    ```cs public static List GetMovies(string searchKeyword) {
    var moviesMasterList = MovieRepository.GetMoviesMasterList();

    if (!string.IsNullOrEmpty(searchKeyword))
    {
        return (from m in moviesMasterList 
                where m.Name.StartsWith(searchKeyword,
               StringComparison.CurrentCultureIgnoreCase)
               select m                            
                ).ToList();
    }else
    {
        return moviesMasterList;           
    }
    

    } ```

添加控制器

现在,我们需要创建一个名为MovieController的 Web API 控制器,其中包含了与电影相关的动作方法。

右键单击控制器文件夹,选择添加,然后选择控制器...。在弹出的添加控制器中,从模板下拉菜单中选择读写动作为空的 API 控制器,命名为MoviesController。确保不选择任何 MVC 控制器模板,只选择 API 控制器模板。

Adding a controller

控制器文件生成后,在GET方法中,添加以下代码:

public List<MovieBO> Get(string id)
{
return MovieTicketsBLL.GetMovies(id);

}
public List<MovieBO> Get()
{
return MovieTicketsBLL.GetMovies("");
}

如果有随请求一起传递的要搜索的关键字,将调用第一个GET方法。此操作方法将从搜索关键字开始检索所有电影。如果没有输入参数,则调用第二个GET方法,返回整个电影列表。添加代码后,一定要引用定义业务对象和业务层的命名空间。

您现在准备测试您的网络应用编程接口方法,该方法将根据您的搜索标准返回电影列表。

这里显示的是用于调用网络应用编程接口服务的网址以及在浏览器上看到的结果:

  • To filter by movie names, insert the following URL in your browser address bar and hit Enter:

    http://localhost/movietickets.webapi/api/movies/get/i

    Adding a controller

  • To see the entire movies list use the following URL and hit Enter:

    http://localhost/movietickets.webapi/api/movies/

    Adding a controller

内容协商

您现在应该已经注意到我们所有的例子都有来自 XML 格式的网络应用编程接口服务的数据。通常,在构建移动网络应用时,JSON 是首选格式,有时我们可能需要使用这些格式或一些其他自定义格式。得益于 Web API 的内容协商功能客户端可以告诉 Web API 服务它接受什么内容格式,Web API 可以自动为相同格式的客户端服务;前提是格式是在 Web API 中配置的。它只支持现成的 XML 和 JSON 格式。

由于以下原因,JSON 是目前首选的数据交换格式:

  • JSON 是轻量级的,带宽不密集。
  • JavaScript 可以直接消费 JSON 并解析为JS对象。不需要格式转换。
  • 对基于 JSON 的水平可扩展数据库的兴趣越来越大,如 MongoDB、CouchDB 等。
  • JSON 几乎可以与任何编程语言互操作,因为它仅限于原始数据类型。

W3C 在其 HTTP 规范中将内容协商定义为:当有多种表示可用时,为给定响应选择适当表示的机制。例如见以下链接:http://www.w3.org/Protocols/rfc2616/rfc2616.txt

网络应用编程接口使用AcceptContent-TypeAccept-Charset HTTP 头进行内容协商。

使用我们在上一节中构建的MoviesController文件,让我们看看这是如何操作的。

高级休息客户端 Chrome 扩展

高级休息客户端 是可用于 chrome 浏览器的扩展,用于创建和测试自定义 HTTP 请求。它支持诸如 JSON/XML 响应查看器、套接字调试、设置自定义头等功能。我们将使用这个简单的轻量级工具来探索网络应用编程接口的内容协商特性。

可以通过以下链接安装在你的 Chrome 浏览器上:

http://goo.gl/EdqIW

如果您不介意使用网络调试工具,如 Fiddler ,请随意使用它,因为我们正在尝试创建一些自定义的 HTTP 请求并比较响应。

接受标题

HTTP Accept 头用于指定响应中客户端可接受的媒体类型。对于 XML,该值设置为application/xml,对于 JSON,该值为application/json,并指定接受所有媒体类型【使用 T2】。

要看到这一点,让我们打开高级休息客户端扩展,向网址http://localhost/movietickets.webapi/api/movies/get/i发送一个 GET 请求,并将添加新标题字段设置为接受标题为application/json

The Accept header

我们的网络应用编程接口服务的响应可以在下面的截图中看到:

The Accept header

如果您将接受头的值更改为application/xml,响应将由网络应用编程接口服务以 XML 形式发送,如浏览器窗口中所示。

内容类型标题

请求中的Content-Type头指定请求正文的 MIME 类型。当设置了Content-Type头(用于POSTPUT请求)并且没有设置接受头时,网络应用编程接口将使用Content-Type头来决定响应的媒体类型。

如果设置了AcceptContent-Type头,则Accept头中指定的媒体类型用于确定响应的媒体类型。在这种情况下,如果网络应用编程接口中没有Accept头中定义的媒体类型的格式化程序,将选择Content-Type头来确定响应媒体类型。

接受字符集标题

Accept-Charset标头用于指示响应的字符编码,不同于AcceptContent-Type标头,后者指定响应的 MIME 类型。

网络应用编程接口使用 UTF 8 和 UTF-16 开箱即用的字符编码,默认使用UTF-8。在请求头中设置Accept-Charset: utf-16将返回UTF-16编码的响应。要基于每个格式化程序在服务器端自定义字符编码,您需要更改MediaTypeFormatter类的SupportEncodings属性。

类型

如果请求包含一个 X-Requested-With 头,表示一个 Ajax 请求,如果没有指定 Accepted 头,服务器可能默认为 JSON。

如前所述,默认情况下,网络应用编程接口只支持 XML 和 JSON 媒体格式化程序。为了支持其他媒体类型,开发人员需要通过从MediaTypeFormatter(异步读/写)类或BufferedMediaTypeFormatter(同步读/写)类派生来创建自定义格式化程序。由于自定义格式化程序的创建不在本书的讨论范围之内,因此我们在此不再详细讨论。如果您有兴趣探索自定义格式化程序,Mike Wasson 的这篇文章将是一个很好的起点:

http://goo.gl/moSOG

作为响应的图像/PDF 文件

在大多数应用中,检索 PDF 或图像文件作为响应是一个常见的要求。通过在您的操作方法中编写几行代码来覆盖由内容协商确定的响应的默认媒体类型,在网络应用编程接口中实现这个场景是非常容易的。

让我们打开ValuesController.cs文件,再添加一个名为GetImage()的动作方法,它会在服务的Content文件夹中返回一个图像文件,如图所示:

public HttpResponseMessage GetImage()
{
    byte[] bytes = System.IO.File.ReadAllBytes(
        HttpContext.Current.Server
        .MapPath("~/Content/Kendo.png"));
    var result = new HttpResponseMessage(HttpStatusCode.OK);
    result.Content = new ByteArrayContent(bytes);
    result.Content.Headers.ContentType 
 = new MediaTypeHeaderValue("img/png");

    return result;
}

也请参考控制器中的这两个名称空间:

System.Web;
System.Web.Http;

这个实现的关键是使用HttpResponseMessage类,这有助于在控制器本身中创建一个原始的 HTTP 响应。然后,我们使用枚举HttpStatusCode.OK设置 HTTP 200 状态代码,并将图像字节设置为响应内容。最后一步是将媒体类型头设置为img/png,回复为HttpResponseMessage类型

现在点击浏览器上的网址http://localhost/movietickets.webapi/api/values/GetImage,这个动作方法会显示优雅的剑道 UI 标志(如果你使用了源代码附带的图片!).

An img/PDF file as response

如果内容是 PDF 文件,我们唯一需要做的改变就是将 MIME 类型更新为application/pdf

保护网络应用编程接口

在真实的- 世界场景中,发布的大多数服务都必须是安全的,只有经过身份验证的客户端才应该能够访问这些服务。默认情况下,在几乎所有企业场景中使用 SSL 来实现传输层安全性,以防止窃听通过网络传输的数据。在本节中,让我们通过实现认证授权来关注应用级安全性。

身份验证是确定用户是他声称的那个人的过程,而授权是验证经过身份验证的用户是否可以执行特定的操作或使用特定的资源。

认证

通过身份验证,我们试图实现的是确保 Web API 服务接收到的每个请求都是用正确的凭据从客户端发送的。实现身份验证有不同的方式,如基本、摘要等。我们将讨论基本身份验证,其中客户端为每个请求在 HTTP 头中发送一个 Base64 编码的用户名和密码。一旦您了解了如何在网络应用编程接口中实现基本身份验证,就很容易挂钩其他形式的身份验证,因为只有身份验证过程不同,而网络应用编程接口挂钩(在哪里进行)也是一样的。

基本认证

基本身份验证,顾名思义,是对 HTTP 请求进行身份验证的最简单、最基本的形式。它不需要服务器端的会话存储或 cookies 的实现。

下图显示了基本身份验证的实现:

Basic authentication

使用消息处理程序进行身份验证

网络应用编程接口希望它的主机(在我们的例子中,是 IIS)对请求进行身份验证。为此,我们可能必须使用任何 ASP.NET 身份验证模块,或者编写一个 HTTP 模块来实现身份验证。

要验证传入请求,主机需要创建一个主体,它是一个类型为IPrincipal的对象,代表当前的安全上下文。然后主机需要通过设置Thread.CurrentPrincipal将这个主体对象附加到当前线程。与主体关联的身份对象有一个名为IsAuthenticated的属性。如果用户通过认证,该属性将返回true;否则它将返回false。由于我们在示例项目中进行网络托管,我们也需要设置HttpContext.Current.User,以使安全上下文一致。

通常,出于各种原因,将身份验证保持在服务之外不是开发人员所希望的。为了在我们的服务中保留身份验证部分,我们需要利用网络应用编程接口消息处理程序。消息处理程序源自抽象类HttpMessageHandler。他们负责接收 HTTP 请求和发送响应。我们可以通过从System.Net.Http.DelegatingHandler派生来创建自定义消息处理程序,并添加我们自己的原始 HTTP 数据操作代码。源自DelegatingHandler有助于通过调用base.SendAsync调用管道中的内部消息处理程序。在消息处理程序中,请求作为HttpRequestMessage可用,响应作为HttpResponseMessage可用。使用处理程序时,可以使用HttpRequestMessage对象轻松完成之前的认证过程。

这些是使用自定义消息处理程序进行基本身份验证所涉及的步骤:

  • 使用消息处理程序捕获传入的请求
  • 如果请求包含授权头:
    • 尝试对用户进行身份验证,如果身份验证成功,请设置主体并允许继续执行
    • 如果认证失败,设置 HTTP 状态码 401,并在响应头中返回 WWW-Authenticate: Basic

类型

WWW-Authenticate: Basic 头用于表示该服务使用的身份验证方案为 Basic。

实施认证

让我们编写一些代码并为我们的电影票网络应用编程接口项目设置身份验证模块:

  1. 创建一个名为Common的文件夹,添加一个名为MovieTicketsPrincipal的类,并添加如下代码:

    ```cs using System.Security.Principal;

    namespace MovieTickets.WebAPI.Common {
    public class MovieTicketsPrincipal: IPrincipal { public string UserName { get; set; } public IIdentity Identity { get; set; } public bool IsInRole(string role) { if (role.Equals("user")) { return true; } else { return false; } } //Constructor public MovieTicketsPrincipal(string userName) { UserName = userName; Identity = new GenericIdentity(userName); }

    }
    

    } ```

  2. Create a folder called Handlers in the Movie Tickets Web API project, add a class called AuthMessagehandler, and add the following code:

    ```cs using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Web; using MovieTickets.WebAPI.Common;

    namespace MovieTickets.WebAPI.Handlers { public class AuthMessagehandler : DelegatingHandler {

        private string _userName;
    
        //Capturing the incoming request by overriding 
        //the SendAsync method
        protected override
    

    System.Threading.Tasks.Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { //if the credentials are validated, //set CurrentPrincipal and Current.User if (ValidateCredentials( request.Headers.Authorization)) { Thread.CurrentPrincipal = new MovieTicketsPrincipal(_userName); HttpContext.Current.User = new MovieTicketsPrincipal(_userName); }

            //Execute base.SendAsync to execute default
            //actions and once it is completed, 
            //capture the response object and add 
            //WWW-Authenticate header if the request 
            //was marked as unauthorized.
             return base.SendAsync(request, cancellationToken)
    

    .ContinueWith(task => { HttpResponseMessage response = task.Result; if (response.StatusCode == HttpStatusCode.Unauthorized && !response.Headers .Contains("WWW-;Authenticate")) { response.Headers .Add("WWW-Authenticate", "Basic"); } return response; }); }

    //Method to validate credentials from Authorization //header value private bool ValidateCredentials( AuthenticationHeaderValue authenticationHeaderVal) { if ( authenticationHeaderVal!= null && !String.IsNullOrEmpty(authenticationHeaderVal.Parameter)) {

                string[] decodedCredentials 
                    = Encoding.ASCII.GetString(
                           Convert.FromBase64String(
                            authenticationHeaderVal.Parameter))
                                .Split(new[] { ':' });
    

    //now decodedCredentials[0] will contain //username and decodedCredentials[1] will //contain password.You need to implement your own //business logic to verify credentials here. //For simplicity, we are hardcoding username //and password here.

                if (decodedCredentials[0].Equals("username") 
                    && decodedCredentials[1].Equals("password"))
                {
                    _userName = "John Doe";
                    return true;//request authenticated.
                }
            }
            return false;//request not authenticated.
        }
    }
    

    } ```

    在这个自定义消息处理程序中,我们使用以下代码覆盖SendAsync方法来捕获传入的请求:

    cs protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { }

    一旦捕获到传入请求,就调用ValidateCredentials()方法来检查包含 Base64 编码格式的用户名和密码的Authorization头值的 HTTP 头。如果找到标题,将提取并验证用户名和密码。在前面的代码中,为了简单起见,我们对照硬编码字符串usernamepassword 进行验证,但通常在现实示例中,将对照活动的目录或数据库检查凭据。如果发现凭证正确,则创建类型为MovieTicketsPrincipal的对象,并将其分配给Thread.CurrentPrincipalHttpContext.Current.User。现在,从服务层的任何地方,都可以访问当前用户的详细信息以进行授权、审核等。一旦响应通过验证,就调用base.SendAsync 向内部处理程序发送请求。内部处理程序异步处理请求并发回响应。

    以下代码确保内部代码仅在收到响应后执行:

    cs base.SendAsync(request, cancellationToken) .ContinueWith(task =>{--- });

    如果响应包含一个未经授权的 HTTP 头,代码会注入一个值为BasicWWW-Authenticate头,通知客户端我们的服务需要基本身份验证。关于被标记为未授权的请求的更多细节将在授权部分讨论。

  3. Now open Global.asax.cs and add the following code in the Application_Start event as the first line:

    cs GlobalConfiguration.Configuration .MessageHandlers.Add(new AuthMessagehandler());

    这段代码将把我们的AuthMessagehandler类添加到MessageHandlers集合中,这样当请求到达时就会调用它。

正在进行认证

要查看我们的身份验证机制的运行情况,让我们使用高级 REST 客户端创建一个对 URL http://localhost/movietickets.webapi/api/movie的新请求,该请求带有标题授权:基本 dxnlcm5 hbwu 6 cgfzc3 vcmq =(其中“dxnlcm5 hbwu 6 cgfzc3 vcmq =”只不过是username:password的 Base64 编码形式),如下图所示:

Authentication in

现在让我们在SendAsync方法中添加几个断点,看看控制流程:

Authentication in

现在我们通过打开请求对象来检查参数方案属性的值:

Authentication in

如您所见,所提供的凭据在消息处理程序中得到了正确验证:

Authentication in

现在让我们继续到下一部分,在那里认证数据将被用于动作方法的访问控制。

授权

到目前为止,我们创建的所有控制器动作方法都是不安全的,这意味着未经身份验证的用户可以访问它们。既然我们已经看到了如何对请求进行身份验证,那么让我们看看如何使用这些信息来授权对某些控制器或动作方法的访问。

网络应用编程接口中的授权过程发生在认证之后的管道中,并且在控制器动作被执行之前。这有助于根据经过身份验证的用户提供访问控制。

对于授权,网络应用编程接口使用授权过滤器,如果请求未通过身份验证,该过滤器将做出错误响应,并阻止受保护的操作方法的执行。

使用 AuthorizeAttribute

网络应用编程接口提供了一个名为AuthorizeAttribute的授权过滤器。该属性验证请求的IPrincipal,检查其Identity.IsAuthenticated属性,如果该值为假,则返回 401 未授权 HTTP 状态,请求的动作方法将不被执行。

AuthorizeAttribute属性可以应用于三个级别:

  • 全球的
  • 控制器
  • 行动级别

全球层面

全局实现是通过在全局过滤器列表中添加AuthorizeAttribute来实现的。有关示例,请参见下面的代码片段:

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

一旦添加了这段代码,只允许经过身份验证的请求调用网络应用编程接口服务中的操作方法。

控制器级别

[Authorize]属性装饰一个控制器将防止控制器中的所有动作方法被未授权的用户访问。

让我们创建一个名为AccountController的新控制器,并添加一个名为 RemoveUser 的动作方法来移除一个用户,然后用[ Authorize]属性修饰控制器:

[Authorize]
    public class AccountController : ApiController
    {
        // GET api/account
        [HttpPost]        
        public bool RemoveUser(int id)
        {
            //custom code to remove user
            return true;
        }
    }

现在,让我们尝试调用RemoveUser操作方法,而不发送适当的凭据,让我们看看会发生什么:

  1. 打开高级休息客户端。
  2. 选择请求类型为POST
  3. Hit the following URL:

    http://localhost/movietickets.webapi/api/account/Removeuser/345

    此操作将在浏览器中显示如下内容:

    Controller level

如您所见,网络应用编程接口以 WWW-Authenticate 标题和以下消息响应了 401 未授权状态代码:

"Message": "Authorization has been denied for this request."

如果出现要求凭证的弹出窗口,只需点击取消

当为这个请求发送了适当的凭证时(也就是说,通过在请求中添加Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=头),您将看到操作被执行并且true被返回。

动作级别

如果您希望在同一个控制器中既有受保护的动作又有不受保护的动作,可以用Authorize属性来修饰单个动作,以便它们需要经过身份验证的请求来调用它们,而它们的动作可以通过匿名请求来执行:

public class AccountController : ApiController
    {
        // GET api/account
        [HttpPost]
        [Authorize] //protected
        public bool RemoveUser(int id){...}

//Unprotected action
        public bool Login(UserBO user){...}
     }

【允许匿名】属性

该属性标记控制器和动作,以便在授权期间跳过AuthorizeAttribute。如果控制器中有大量受保护动作,极少数不受保护,控制器可以用[Authorize]修饰,不受保护的动作可以用[AllowAnonymous]属性修饰。

因此,动作级授权中提到的代码可以这样编写:

[Authorize]
 public class AccountController : ApiController
    {
        // GET api/account
        [HttpPost]        
        public bool RemoveUser(int id){...}

         [AllowAnonymous]
        public bool Login(UserBO user){...}
     }

角色和用户检查

也可以过滤某些角色和用户,以便只授予他们对操作方法的访问权限。

角色检查可以通过两种方式完成:

  • 使用User.IsInRole("Admin")内的动作方法代码。用户是当前主体,在ApiController基类中定义。
  • 在控制器和动作上使用[Authorize(Roles = "Admin")]

类型

如果有多个角色,并且您需要检查用户是否处于任何角色,请使用[Authorize(Roles = "Admin, PowerUser")]

同样,用户也可以过滤:

  • 使用User.Identity.Name.Equals("Bob")内的动作方法代码
  • 在控制器和动作上使用[Authorize(User = "Bob")]

自定义授权属性

在真实的- 世界应用中,使用复杂的逻辑来实现授权(例如,您希望防止某个范围的 IP 地址调用某些操作方法),内置的AuthorizationAttribute可能不够,我们可能不得不依赖于创建自己的自定义授权过滤器。这可以通过三种方式实现:

  • 通过异步扩展AuthorizeAttribute
  • 通过同步延伸AuthorizationFilterAttribute
  • 通过异步实现IAuthorizationFilter

让我们看看如何扩展AuthorizeAttribute属性并创建自定义授权属性:

public class AdminUsersOnlyAttribute : AuthorizeAttribute
{
    protected override bool IsAuthorized(HttpActionContext context)
    {
        var currentPrincipal = Thread.CurrentPrincipal;
        // custom logic to check whether user is admin or not
    }

    protected override void HandleUnauthorizedRequest(
      HttpActionContext actionContext)
    {
        // custom response for unauthorized request 
    }
}

现在控制器和动作方法可以用[AdminUsersOnly]属性修饰,防止非管理员用户访问。

为了降低本书的复杂性,我们将AuthorizationFilterAttribute 的扩展和的实现IAuthorizationFilter 留给你作为练习。

总结

在本章中,我们学习了 ASP.NET 网络应用编程接口的特性,以及如何创建和配置新的网络应用编程接口服务。我们还了解了网络应用编程接口如何实现路由、参数绑定、内容协商和安全性。对于示例电影票应用,我们创建了一个服务工作结构,其中包含了我们在本章中学习的所有功能。在下一章中,我们将探索剑道用户界面框架的元素,并将网络应用编程接口与我们的剑道用户界面移动应用相集成。