OData

OData、实体框架和 Windows Azure 访问控制

Sean Iannuzzi

下载代码示例

在本文中,我将阐述使用实体框架(通过 Windows Communication Foundation (WCF) RESTful 服务公开并用 Windows Azure 访问控制服务 (ACS) 保证安全),实施开放数据协议 (OData)。

如同大多数开发人员,我经常发现自己试图利用各种新方法综合利用多种技术,以便尽可能高效地完成项目,同时还要提供一种灵活、易于维护的解决方案。 这样做可能很困难,当项目需要快速安全地公开数据时尤其如此。

最近我需要为一个现有数据库和 Web 应用程序创建一个安全的 Web 服务。 我真的不想实施代码的所有 CRUD(创建、读取、更新、删除)操作。 仅仅创建自定义服务约定、操作约定和数据约定就非常诱人,这样可以准确实现如何公开数据,以及其他人如何通过服务使用这些数据。 但是我知道必须采取一种更为有利的办法。 我开始研究完成这项工作的各种方法,并看到 OData (我喜欢称其为“哦数据”)的潜力。 问题在于 OData 本身并不安全,这是我不能接受的,所以我需要在 OData 服务之上添加一个安全层,这样我才放心 OData 是有安全保障的。 当我开始着手之时,我发现了 ACS,ACS 非常适于实施基于云的联合身份验证和授权服务,这正是我需要的。 然后我觉得很得意。 我意识到如果我将 ACS 与 OData 结合起来,我就得到解决方案了。

现在,我的确考虑实施自定义服务约定,实施这种方法是可行的,尤其是当数据模型前面需要一个抽象层以及需要保护数据库实体以防直接向服务消费者公开的情况下。 然而,鉴于其非常耗时——创建关于如何使用此服务的适当文档,以及投入额外的努力以设置安全性(“MessageCredential”和“TransportWithMessageCredentials”),所以这个项目可能会很快失控。 我还担心为了支持如何使用这些服务而因为这样或那样的原因需要或请求额外的方法,这样会再次增加时间、维护和自定义。 即使服务的实施直接使用了实体框架与 ADO.NET,仍然可能需要进行代码的所有 CRUD,以便保持数据层的同步。 假设有几十个表,这种工作可能非常单调乏味。 而且,创建并维护任何附加的文档和实施详情以便让最终用户使用我的服务,只会让这项工作变成一个更加复杂的主张,难以管理。

更简便的方法

我确认了主要技术之后,我开始寻找其他技术来填补空缺并帮助构建一套结合紧密的解决方案。 目标是限制需要编写或维护的代码数量,同时安全地公开我的 OData WCF RESTful 服务。 我结合的技术是: ACS、OData、实体数据模型、WCF 数据服务(具有实体许可)及一个自定义 Windows Azure 安全实施。 每项技术都具有各自的重要价值,但结合起来,他们的价值将大幅增加。 图 1 大体显示了部分技术的工作原理概述。

High-Level Overview of ACS with a Security Intercept
图 1 具备安全性截获的 ACS 简要概述

在试图合并所有这些技术之前,我必须回头,仔细了解每种技术以及这些技术会对本项目有什么影响。 然后我清晰地掌握如何整合这些技术,以及其他人通过其他技术来使用我的服务还需要哪些条件。

什么是 ACS?

ACS 是 Windows Azure 平台的一个组件。 使用 ACS 可以设置我自己基于云的联合身份验证和授权提供程序,以用于保证 OData WCF 服务的安全,但 ACS 还可以用于保证任何应用程序的安全。 ACS 是一种基于云的服务,有助于在需要在多个应用程序、服务或产品(无论是跨平台还是跨域)上实施单一登录 (SSO) 时弥合安全性差距,支持多种 SSO 实施。 通过 Windows Azure 帐户可以获取更多信息。 您可以在windowsazure.com上注册免费试用。 欲知更多关于 ACS 的详情,请访问 bit.ly/zLpIbw

什么是“OData”,为什么我会使用它?

OData 是一种基于 Web的协议,使用标准化语法查询和更新数据,以及公开数据。 OData 利用 HTTP、XML、JSON 及 Atom 发布协议提供不同的数据访问途径。 实施 OData 与实体框架和 WCF 数据服务具有多种好处。

我开始疑惑自己为什么使用 OData 而不是自定义 WCF 约定。 答案非常简单。 最实际的原因就是利用已经可用的服务文档,并使用标准化语法(标准化语法支持如何访问我服务中的数据)。 编写了几十个服务之后,似乎由于提供自定义服务,我总是需要添加一种额外的方法。 而自定义服务的使用者往往会要求更多的功能。

 有关 OData 和 OData URI 约定的详细信息,请访问以下网站:

OData 与实体框架和 WCF 数据服务

使用 OData 配合 WCF 数据服务和实体框架可以公开标准功能,以支持通过一种实施代码极少的方法进行数据的检索和保存。 当我首先开始为数据服务封装格式 (EDMX) 创建实体数据模型时,并通过数据服务将其与 WCF 服务链接起来,我对此有点怀疑。 但是,这样做非常顺利。 我在 EDMX 中包括的所有实体都自动包括在内并在 RESTful 实施中的 WCF 服务中公开。 图 2 显示了一些示例代码。

图 2 实施 OData WCF 数据服务

using System.Data.Services;
using System.Data.Services.Common;
namespace WCFDataServiceExample
{
  public class NorthwindService : DataService<NorthwindEntities>
  {
    // This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
    {
      // Give full access to all of the entities.
config.SetEntitySetAccessRule("*", EntitySetRights.All);
      // Page size will change the max number of rows returned.
config.SetEntitySetPageSize("*", 25);
      config.DataServiceBehavior.MaxProtocolVersion =
        DataServiceProtocolVersion.V2;
    }
  }
}

我创建了 EDMX 并将其链接到数据库(Northwind 示例数据库)中的一些表格。 然后我将数据库实体链接到 WCF 数据服务,使用“SetEntitySetAccessRule”方法(所有实体的通配符属性为“*”)公开所有实体。 这样可以让我在实体上对读、写和查询访问设置不同的权限,以及设置页面大小,如图 2所示。 所显示的代码就是 OData WCF 数据服务代码的完整实施。

服务约定、操作约定和数据约定主要通过所提供服务的初始化中的配置来控制。 在初始化方法中,我能够对我如何公开我的实体以及我想对任何想要使用我的服务的人提供的访问级别设置不同的权限。 我甚至可以利用 T4 模板在具有自定义实体名称的实体之上创建一个抽象层或模板层。 这样可以为使用我的服务的消费者提供额外的清晰级别。 我甚至可以对特定表格设置权限,或者可以按照适当的安全性设置来设置表格名称,以提供较低级别的保护。 以下是向客户表格提供读权限的示例:

config.SetEntitySetAccessRule("Customer",
  EntitySetRights.AllRead);

许多不同的安全实施可以通过 OData 和 WCF 数据服务来启用,但是现在我只关心如何使用 ACS 配合数据服务访问规则来保护我的 WCF 服务。

图 3 显示了所使用技术的简略列表,以及为何使用这些技术的部分原因。

图 3 所使用的技术及原因

技术 为何使用
ACS 提供了一种方法,可以基于云的联合安全模块进行身份验证和授权,以保证服务的安全性。
OData 提供一种标准语法,用于查询和更新数据,同时利用了 HTTP、JSON、Atom 发布协议 及 XML 等通用技术。
实体数据模型 提供了一种快捷的方法,为数据库层创建公共数据访问,同时为数据库中的表格提供可序列化的数据约定。
具有实体权限的 WCF 数据服务 凭着适当的 CRUD 权限级别,公开实体框架数据约定,以作为 WCF RESTful 服务
自定义 Windows Azure 安全性实施 保证服务(在本例中是 OData)免于被使用,而无须应用适当的安全级别,例如令牌或证书。

对于每种技术,总是会在项目的基础上进行权衡,但是我发现整合各种技术可以节省预先的设置时间,减少维护工作,同时要求的代码数量较少,好处很多——当我需要安全地公开数据,并用标准化语法提供通用数据访问方法时,尤其如此。

综合来讲

在清晰理解综合使用 OData、实体框架及 WCF 数据服务之后,我可以利用 ACS,将某些附加的安全功能应用于这种技术。 有几种方法可以保证我的服务免于被人访问,包括对实体设置不同的权限,或添加查询拦截器,以防止服务的使用,或者控制服务的使用方法。

然而,实施查询拦截器或设置权限会非常单调乏味,因此首先在我的服务之上添加一个安全层,以免被人使用,而不是编写额外的代码。 实施通用安全机制,允许受信任的人士或外部公司访问我的服务是最理想的。 反过来,我可以综合使用这种安全机制与实体保护,为我的服务提供最安全的实施和最大的灵活性。

使用这种方法要求服务的使用者首先通过 ACS 进行身份验证,然后获得一个有效的访问令牌。 如果没有此令牌,将禁止使用服务。 任何人获准访问我的服务之前,请求报头中要有有效的访问令牌。 一旦服务的使用者获得授权,我便对实体应用细粒度安全性,以确保只有被授权者可以访问数据或我的实体。

ACS 安装与配置

实施 ACS 需要进行一些安装和配置。 图 4 显示了我为本例设置的项目列表。

图 4 ACS 安装

配置
ACS 命名空间 WCFoDataACS
回复方应用程序

名称: wcfodataacsexampleDevelopment

模式: 保留默认值(手动输入设置)

领域: http://localhost/WCFDataServiceExample/<servicename>.svc

返回 URL: 保留默认值(空白)

错误 URL: 保留默认值(空白)

令牌格式: SWT

令牌加密策略: 保留默认值(无)

令牌生存期: 保留默认值(600)

身份验证设置

标识提供者: 取消选中 Windows Live ID

规则组: 选中“创建新规则组”

注: 我为开发、测试和生产创建了不同的设置。

令牌签名设置

单击“生成”按钮

生效日期: 保留默认值

到期日期: 保留默认值

规则组 注: 根据我的设置,规则组将自动创建,但是我仍然需要添加声明配置。
声明配置

如果部分:

访问控制系统: 已选择

输入声明类型: 保留默认值(任意)

输入声明值: 保留默认值(任意)

然后部分:

输出声明类型: 保留默认值(通过输入声明类型)

输出声明值: 保留默认值(通过输入声明值)

规则信息:

描述: 保留默认值或输入描述

Windows 中的服务标识

名称: 为他人提供的用户名(在本例中我使用 wcfodataacsDevUser)

描述: 保留默认值(或输入用户的描述)

领域: http://localhost/WCFDataServiceExample/<servicename>.svc

凭据设置:

类型: 选择密码

密码: 输入密码

生效日期: 保留默认值

到期日期: 保留默认值

注: 有几种方法可以对用户进行身份验证,以使用所创建服务,但是为了简便起见,我使用密码作为凭据类型。 还有其他方法,例如使用 x509 证书或对称密钥,可以提供更高的安全级别,但在本例中我试图保持基本即可。

完成 ACS 安装后,我将能够保证 OData WCF RESTful 服务的安全。 在我可以保证安全之前,我首先实施一个自定义安全模块,可以拦截请求并验证安全性以防未经授权的访问。

ACS 安全实施

例如,我使用自定义 HTTP 模块实施安全性。 这样可以拦截发送到我的服务的任何请求,并验证是否进行适当的身份验证和授权。 没有这个 HTTP 模块,我的服务在数据服务配置中设置基础上,只在实体级别是安全的。

在这种情况下,我用 ACS 保证这些服务的安全;因此请求被拦截,然后检查是否达到适当的安全级别,以确保服务的使用者获得了适当的授权级别。 如前所述,服务的使用者获得授权之后,便在实体级别实施细粒度安全性。

在实施 IHTTPModule 接口时,我选择添加一些额外的功能,便于我公开部分服务元数据,以让服务的使用者自动生成类(类似于添加任何其他 Web 服务的行为)。 我添加了这些代码部分,作为可配置的属性,可启用或禁用以提高安全性,进行测试及简化集成工作。

图 5 显示了拦截请求并执行适当安全验证的代码。

图 5 安全验证

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Web;
using Microsoft.AccessControl2.SDK;
namespace Azure.oAuth.SecurityModule
{
  internal class SWTModule : IHttpModule
  {
    // Service namespace setup in Windows Azure
    string _serviceNamespace = 
      ConfigurationManager.AppSettings["serviceNamespace"];
    // ACS name
    string _acsHostName = ConfigurationManager.AppSettings["acsHostname"];
    // The key for which the service was signed
    string _trustedSigningKey =
      ConfigurationManager.AppSettings["trustedSigningKey"];
    // The URL that was setup in the rely party in Windows Azure
    string _trustedAudience = 
      ConfigurationManager.AppSettings["trustedAudience"];
    // Setting to allow the metadata to be shown
    bool _enableMetaData =  
       Convert.ToBoolean(ConfigurationManager.AppSettings["enableMetadata"]);
    // Setting to disable or enable the security module
    bool _disableSecurity =
      Convert.ToBoolean(ConfigurationManager.AppSettings["disableSecurity"]);
    const string _metaData = "$metadata";
    private void context_BeginRequest(object sender, EventArgs e)
    {
      if (!_disableSecurity)
      {
        string tempAcceptableURLs = String.Empty;
        // Check if the audiencename has trailing slash
        tempAcceptableURLs = _trustedAudience.ToLower();
        if (tempAcceptableURLs.Substring(_trustedAudience.Length - 1, 1) == "/")
        {
          tempAcceptableURLs =
            tempAcceptableURLs.Substring(0, _trustedAudience.Length - 1);
        }
        // First check if the person is requesting the WSDL or .svc
        if (_enableMetaData != false
          && HttpContext.Current.Request.Url.AbsoluteUri.ToLower() !=
          tempAcceptableURLs
          && HttpContext.Current.Request.Url.AbsoluteUri.ToLower() !=
          tempAcceptableURLs + _metaData
          && HttpContext.Current.Request.Url.AbsoluteUri.ToLower() !=
          tempAcceptableURLs + "/"
          && HttpContext.Current.Request.Url.AbsoluteUri.ToLower() !=
          tempAcceptableURLs + "/" + _metaData)
        {
          // SWT Validation...
// Get the authorization header
          string headerValue =
            ttpContext.Current.Request.Headers.Get("Authorization");
          // Check that a value is there
          if (string.IsNullOrEmpty(headerValue))
          {
            throw new ApplicationException("unauthorized-1.1");
          }
          // Check that it starts with 'WRAP'
          if (!headerValue.StartsWith("WRAP "))
          {
            throw new ApplicationException("unauthorized-1.2");
          }
          // ...
<code truncated> ...
}
      }
    }
  }
}

Windows Azure SDK

我从 Windows Azure SDK 拉来一个类,为此实施执行令牌验证。 有关此项目,请访问 bit.ly/utQd3S。 安装 SDK 之后,我将名为“tokenvalidator.cs”的文件复制到一个新项目。 在这个类中,我调用验证方法,以确定是否通过 ACS 中配置的信息对用户进行授权。 为了简化此实施,我用所需的唯一一个安全机制,创建了一个自定义 DLL。 创建了程序集之后,我需要的就是通过我的 OData WCF 服务对安全 DLL 的引用。 其结果是: 一个受保护和安全的实施。

安全 OData 服务的实施

在部署了额外的安全增强措施之后,保证 OData WCF 服务的安全就变得简单了。 所需的就是对“Azure.AccessControl.SecurityModule”程序集的引用,并添加到附加的配置设置中。 然后可以启用安全功能。 图 6 显示了安全性配置设置。

图 6 安全性配置设置

<appSettings>
  <add key="acsHostName" value="accesscontrol.windows.
net" />
  <add key="serviceNamespace" value="Service Namespace" />
  <add key="trustedAudience"
    value="http://localhost/WCFDataServiceExample/NorthwindService.svc/" />
  <add key="trustedSigningKey" value="Trusted Signing Key" />
  <add key="enableMetadata" value="true" />
  <add key="disableSecurity" value="false"/>
</appSettings>
<system.webServer>
  <validation validateIntegratedModeConfiguration="false" />
  <modules runAllManagedModulesForAllRequests="true">
    <add name="SWTModule" type="Azure.AccessControl.SecurityModule.SWTModule,
      Azure.AccessControl.SecurityModule" preCondition="managedHandler" />
  </modules>
</system.webServer>

根据安全性设置,可以限制服务的使用者仅看到元数据。 这非常有用,因为用户仍然可以引用代码中的实体对象和属性,从而简化了实施。 为了禁用元数据,我将”enableMetadata“属性设置为 false,因此服务的使用者再也不能访问元数据。 如果服务的使用者只是通过客户端侧代码进行访问,我不会启用元数据,因为这没有必要。 启用了元数据后,此服务看起来和普通的 Web 服务一样,但是没有适当的身份验证和授权,无法使用,如图 7所示。

OData WCF Service with Metadata Exposed
图 7 公开了元数据的 OData WCF 服务

这几乎与直接对实施代码使用实体框架的效果一样,只有些许的小差异。 要添加的主要代码段是在向 OData WCF 服务发送数据时请求报头中所需的令牌。 我将解释安全机制的工作原理。 首先,安全机制检查报头是否有有效的令牌,然后检查是否所有部件都正常,例如目标受众、令牌到期和令牌值。 接下来,对请求进行授权,然后对服务的调用成功完成。 拦截此请求,然后将任何数据返回服务的使用者,这样确保了服务的调用者必须获得有效的令牌才能获准访问任何数据。

在这一点上,根据实体对象上需要的安全级别,服务的使用者能够依据所设定的安全设置,执行服务所公开的任何功能。 在未启用安全性的情况下,服务的使用者会收到一条例外,表示所执行的操作是不允许的。

与传统实体框架代码不同,需要实施更多的逻辑才能调用 Windows Azure 保障安全的 OData 服务。 有了 HTTP 模块保护此服务,我需要确保我首先对 Windows Azure 进行身份验证并收到一个有效的访问令牌,然后才能调用 OData 服务。 从 ACS 收到的令牌将通过每个请求的请求报头,以确保 OData 服务的安全。 图 8 显示了一个示例请求。

图 8 示例令牌请求

// Request a token from ACS
using (WebClient client = new WebClient())
{
  client.BaseAddress = string.Format("https://{0}.{1}",
    _accessControlNamespace, _accessControlHostName);
  NameValueCollection values = new NameValueCollection();
  values.Add("wrap_name", wrapUsername);
  values.Add("wrap_password", wrapPassword);
  values.Add("wrap_scope", scope);
  byte[] responseBytes =
    client.UploadValues("WRAPv0.9/", "POST", values);
  response = Encoding.UTF8.GetString(responseBytes);
  Console.WriteLine("\nreceived token from ACS: {0}\n", response);
}

从 Windows Azure 收到令牌并且成功对用户进行身份验证和授权之后,令牌将从 ACS 返回,以用于所有未来的请求,直至令牌到期为止。 在这一点上,实施实体框架与连接本地数据库或我网络上的数据库几乎是相同的。 图 9 显示了凭借访问令牌使用 OData 服务。

图 9 以 Windows Azure 访问令牌使用 OData 安全服务

// First things first: I obtain a token from Windows Azure
_token = GetTokenFromACS(_rootURL + "NorthwindService.svc");
// Now that I have a valid token, I can call the service as needed
Uri uri = new Uri(_rootURL + "NorthwindService.svc/");
try
{
  var northwindEntities = new ODataServiceClient.NorthwindEntities(uri);
  // Add the event handler to send the token in my request header
  northwindEntities.SendingRequest += new
    EventHandler<SendingRequestEventArgs>(OnSendingRequest);
  // Sample selecting data out ...
var customersFound = from customers in northwindEntities.Customers
    select customers;
  foreach (var customer in customersFound)
  {
    // custom process ...
// ...
<code truncated> ...
}
    // Add new data in ...
var category = oDataServiceClient.Category.CreateCategory(0, "New category");
    northwindEntities.AddToCategories(category);
    northwindEntities.SaveChanges();
}
catch (DataServiceRequestException e)
{
  // Trap any data service exceptions such as a security error
  // In the event that the security does not allow an insert,
  // a forbidden error will be returned
  // ...
}

通过客户端侧脚本实施代码也像对我的服务端点进行 AJAX 调用一样简单。 图 10 显示了从客户端侧脚本使用 OData 安全服务。

图 10 从客户端侧脚本使用 OData 安全服务

// Parse the entity object into JSON
var jsonEntity = window.JSON.stringify(entityObject);
$.support.cors = true;
// Asynchronous AJAX function to create a Cateogory using OData
$.ajax({
  type: "POST",
  contentType: "application/json; charset=utf-8",
  datatype: "jsonp",
  url: serverUrl + ODATA_ENDPOINT + "/" + odataSetName,
  data: jsonEntity,
  beforeSend: function (XMLHttpRequest) {
  // Specifying this header ensures that the results will be returned as JSON
  XMLHttpRequest.setRequestHeader("Accept", "application/json");
  XMLHttpRequest.setRequestHeader("Authorization", token);
  },
  success: function (data, textStatus, XmlHttpRequest) {
  if (successCallback) {
    successCallback(data.d, textStatus, XmlHttpRequest);
    }
  },
  error: function (XmlHttpRequest, textStatus, errorThrown) {
  if (errorCallback)
    errorCallback(XmlHttpRequest, textStatus, errorThrown);
  else
    errorHandler(XmlHttpRequest, textStatus, errorThrown);
  }
});

RESTful 服务在实施上提供了更大的灵活性,并且通过 Java 或其他客户端侧脚本或 API 易于使用。 要使用服务,仍然需要身份验证和令牌,但是由于查询语法,OData 是标准方法,无论在何种平台上。 图 11 显示了在 Java 中凭借 Windows Azure 访问令牌使用 OData 安全服务。

图 11 在 Java 中凭借 Windows Azure 访问令牌使用 OData 安全服务

String serviceMethodUrl =  
  "http://localhost/WCFDataServiceExample/NorthwindService.svc/Categories?";
GetMethod method = new GetMethod(serviceMethodUrl + "$top=1");
method.addRequestHeader("Authorization", 
  "WRAP access_token=\"" + authToken + "\"");
try
{
  int returnCode = client.executeMethod(method);
  // ...
<code truncated> ...
br = new BufferedReader(new 
    InputStreamReader(method.getResponseBodyAsStream()));
  String readLine;
  while(((readLine = br.readLine()) != null))
  {
    //System.err.println(readLine);
    result += readLine;
  }
}

总之,我经常发现需要公开数据,但是这种公开需要某种程度的安全性,以防未经授权的访问。 使用 ACS 可以支持这种需求,利用基于云的联合服务,不仅保护我的 OData WCF 数据服务,还保护其他应用程序。

也就是说,仅仅使用 WCF 数据服务就需要对有待公开的数据实施个别数据约定和查询拦截器。 使用实体框架配合 WCF 数据服务能够利用数据库实体,例如数据约定,而这些约定的格式早已设置妥当(可序列化对象,可通过 OData 进行访问)。 这个难题的最后部分是确保我的 OData WCF RESTful 服务受到保护,不会遭受未经授权的访问。 使用 ACS、OData 以及 WCF RESTful 服务包装的实体框架,可以快速地公开我的数据,同时在额外的安全层下使用标准查询语法。

Sean Iannuzzi 是 The Agency Inside Harte-Hanks 的解决方案架构师,利用最佳实践创建企业、系统和软件解决方案。 他喜欢学习新技术并寻找各种方法利用技术帮助企业和开发人员解决问题。 我的博客地址是 weblogs.asp.net/seaniannuzzi,您可以在 Twitter twitter.com/seaniannuzzi 上关注他。

衷心感谢以下技术专家对本文的审阅: Danilo Diaz