다음을 통해 공유


SharePoint 项目的 Azure 自定义声明提供程序(第 2 部分)

原文发布于 2012 年 2 月 15 日(星期三)

在本系列的第 1 部分中,我简要地阐述了此项目的目标,简单地说就是,使用 Windows Azure 表存储作为 SharePoint 自定义声明提供程序的数据存储。该声明提供程序将使用 CASI 工具包(该链接可能指向英文页面)从 Windows Azure 检索所需的数据,以便提供人员选取器(即通讯簿)和键入控件名称解析功能。

第 3 部分中,我创建了 SharePoint 场中使用的所有组件。这包括基于 CASI 工具包的自定义组件,用于管理 SharePoint 和 Azure 之间的所有通信。还有一个自定义的 Web 部件,可捕获有关新用户的信息并将其推送到 Azure 队列中。最后是自定义声明提供程序,通过 WCF 与 Azure 表存储通信(借助 CASI 工具包自定义组件)以启用键入控件和人员选取器功能。

现在让我们对此应用场景做一点补充。

这种解决方案非常适合极其常见的应用场景,即您需要一个几乎不用管理的 Extranet。例如,您希望您的合作伙伴或客户能够点击您的网站,申请一个帐户,然后能够自动“设置”该帐户(对于不同的人员,“设置”的含义大不相同)。我们将以这些作为基准应用场景,当然我们还会使用公有云资源做些事情。

首先,让我们了解下我们将自己开发的云组件:

  • 用于记录要支持的所有声明类型的表
  • 用于记录人员选取器的所有唯一声明值的表
  • 队列,可用于发送应添加到唯一声明值列表的数据
  • 某些数据访问类,用于从 Azure 表读写数据以及将数据写入队列
  • Azure 辅助角色,该角色将从队列读取数据并填充唯一声明值表
  • 用作终结点的 WCF 应用程序,SharePoint 场通过该终结点进行通信,以获取声明类型的列表、搜索声明、解析声明以及将数据添加到队列

现在我们开始详细地介绍每一项内容。

声明类型表

声明类型表用于存储自定义声明提供程序可以使用的所有声明类型。在此应用场景中,我们将只使用一种声明类型,即身份声明,也就是此例中的电子邮件地址。您可以使用其他声明,但是为了简化此应用场景,我们就使用这种声明。在 Azure 表存储中,您将类的实例添加到表中,所以我们需要创建一个类来对声明类型进行说明。重申一次,您可以对 Azure 中的同一表使用不同类型的类实例,但为了简单起见,这里我们不那么做。该表要使用的类如下所示:

namespace AzureClaimsData

{

    public class ClaimType : TableServiceEntity

    {

 

        public string ClaimTypeName { get; set; }

        public string FriendlyName { get; set; }

 

        public ClaimType() { }

 

        public ClaimType(string ClaimTypeName, string FriendlyName)

        {

            this.PartitionKey = System.Web.HttpUtility.UrlEncode(ClaimTypeName);

            this.RowKey = FriendlyName;

 

            this.ClaimTypeName = ClaimTypeName;

            this.FriendlyName = FriendlyName;

        }

    }

}

 

我不打算全面介绍使用 Azure 表存储的基本知识,因为已经有很多资源介绍过了。如果您需要有关 PartitionKey 或 RowKey 的含义以及如何使用它们的更多详细信息,您的简单易用的本地 Bing 搜索引擎可能会有所帮助。这里要指出的是,我将对要为 PartitionKey 存储的值进行 URL 编码。为什么呢?因为在这里,我的 PartitionKey 是声明类型,它可以采用多种格式,例如 urn:foo:blah、https://www.foo.com/blah 等等。如果声明类型包含正斜杠,Azure 则无法存储带那些值的 PartitionKey。所以我们采用 Azure 接受的友好格式对它们进行编码。正如我在上面所述,本例中我们将使用电子邮件声明,所以其声明类型为 https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress

唯一声明值表

唯一声明值表用于存储所有的唯一声明值。在本例中,我们只存储一种声明类型(即身份声明),按照定义,所有的声明值都将是唯一的。不过,我是出于可扩展性考虑才采用了此方法。例如,假设以后您想要对此解决方案使用角色声明。当然,存储诸如“员工”、“客户”或其他等角色声明上千次毫无意义;对于人员选取器,只需知道值存在,即可在选取器中使用值。随后,不管谁拥有它,我们只需在为站点授予权限时允许使用它即可。所以,基于以上这些,存储唯一声明值的类将如下所示:

namespace AzureClaimsData

{

    public class UniqueClaimValue : TableServiceEntity

    {

 

        public string ClaimType { get; set; }

        public string ClaimValue { get; set; }

        public string DisplayName { get; set; }

 

        public UniqueClaimValue() { }

 

        public UniqueClaimValue(string ClaimType, string ClaimValue, string DisplayName)

        {

            this.PartitionKey = System.Web.HttpUtility.UrlEncode(ClaimType);

            this.RowKey = ClaimValue;

 

            this.ClaimType = ClaimType;

            this.ClaimValue = ClaimValue;

            this.DisplayName = DisplayName;

        }

    }

}

 

这里需要注意几件事。首先,与前面的类一样,PartitionKey 使用 UrlEncoded 值,因为它将成为声明类型并且包含正斜杠。其次,在使用 Azure 表存储时,我经常看到数据不是很规范,因为它不像 SQL 那样,存在 JOIN 概念。从技术上来说,您可以在 LINQ 中执行 JOIN,但在处理 Azure 数据时,LINQ 中的很多功能都被禁止了(或执行效果太差),所以还不如就让它不规范算了。如果你们对此有其他的想法,请通过评论进行交流,集思广益嘛。所以本例中,显示名称将为“电子邮件”,因为那是我们将在此类中存储的声明类型。

声明队列

声明队列非常简单,我们要在该队列中存储“新用户”的请求,然后 Azure 辅助流程将从队列中对其进行读取并将数据移入唯一声明值表中。这样做的主要原因是因为使用 Azure 表存储有时会非常的慢,但将某项目放在队列中则非常快。使用此方法可以尽可能地降低对我们 SharePoint 网站的影响。

数据访问类

使用 Azure 表存储和队列的一个非常现实的方面是您总是需要编写自己的数据访问类。对于表存储,您必须编写数据上下文类和数据源类。我不打算在这上面花费大量时间,因为您可以上网阅读相关的资料,另外我还在本文中随附了 Azure 项目的源代码,所以您可以自行根据需要进行了解。

但这里我需要指明一件很重要的事情,虽然这只是个人选择的问题。我喜欢将我所有的 Azure 数据访问代码汇集到一个单独的项目中。通过这种方式我可以将其编译成其自己的程序集,我甚至可以从非 Azure 项目使用它。例如,在我要上载的示例代码中,您将会看到一个 Windows 窗体应用程序,我用它测试 Azure 后端的不同部分。它对于 Azure 一无所知,但具有对某些 Azure 程序集和我的数据访问程序集的引用。我可以在该项目中使用它,就像在我的 WCF 项目(我用它对 SharePoint 进行前端数据访问)中一样轻松。

下面是一些有关数据访问类的特殊事项:

  • ·         我使用一个单独的“容器”类接收返回的数据,即声明类型和唯一声明值。我说的容器类指的是我具有一个简单的类,此类具有类型为 List<> 的公共属性。请求数据时,我返回此类,而不仅仅是结果的 List<>。之所以这么做,是因为当我从 Azure 返回 List<> 时,客户端仅获得该列表中的最后一个项目(当您从本地托管的 WCF 中执行相同的操作时,则工作正常)。所以,为了解决此问题,我在类中返回声明类型,如下所示:

namespace AzureClaimsData

{

    public class ClaimTypeCollection

    {

        public List<ClaimType> ClaimTypes { get; set; }

 

        public ClaimTypeCollection()

        {

            ClaimTypes = new List<ClaimType>();

        }

 

    }

}

 

唯一声明值返回的类如下所示:

namespace AzureClaimsData

{

    public class UniqueClaimValueCollection

    {

        public List<UniqueClaimValue> UniqueClaimValues { get; set; }

 

        public UniqueClaimValueCollection()

        {

            UniqueClaimValues = new List<UniqueClaimValue>();

        }

    }

}

 

 

  • ·         数据上下文类非常简单,没有什么独特之处(正如我的朋友 Vesa 所说);它如下所示:

 

namespace AzureClaimsData

{

    public class ClaimTypeDataContext : TableServiceContext

    {

        public static string CLAIM_TYPES_TABLE = "ClaimTypes";

 

        public ClaimTypeDataContext(string baseAddress, StorageCredentials credentials)

            : base(baseAddress, credentials)

        { }

 

 

        public IQueryable<ClaimType> ClaimTypes

        {

            get

            {

                //this is where you configure the name of the table in Azure Table Storage

                //that you are going to be working with

                return this.CreateQuery<ClaimType>(CLAIM_TYPES_TABLE);

            }

        }

 

    }

}

 

  • ·         在数据源类中,我的确采用了略微不同的方法与 Azure 进行连接。我在网上看到的大部分示例都是使用某些注册设置类(它不叫这个名字,我只是忘记叫什么了)读取凭据。在这里使用该方法有一个问题,就是我没有特定于 Azure 的上下文,因为我想在 Azure 外部使用我的数据类。所以,我只是在我的项目属性中创建一个设置,在该设置中包括连接到我的 Azure 帐户所需的帐户名和密钥。这样用于连接 Azure 存储的两个数据源类均已编写完毕,如下所示:

 

        private static CloudStorageAccount storageAccount;

        private ClaimTypeDataContext context;

 

 

        //static constructor so it only fires once

        static ClaimTypesDataSource()

        {

            try

            {

                //get storage account connection info

                string storeCon = Properties.Settings.Default.StorageAccount;

 

                //extract account info

                string[] conProps = storeCon.Split(";".ToCharArray());

 

                string accountName = conProps[1].Substring(conProps[1].IndexOf("=") + 1);

                string accountKey = conProps[2].Substring(conProps[2].IndexOf("=") + 1);

 

                storageAccount = new CloudStorageAccount(new StorageCredentialsAccountAndKey(accountName, accountKey), true);

            }

            catch (Exception ex)

            {

                Trace.WriteLine("Error initializing ClaimTypesDataSource class: " + ex.Message);

                throw;

            }

        }

 

 

        //new constructor

        public ClaimTypesDataSource()

        {

            try

            {

                this.context = new ClaimTypeDataContext(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials);

                this.context.RetryPolicy = RetryPolicies.Retry(3, TimeSpan.FromSeconds(3));

            }

            catch (Exception ex)

            {

                Trace.WriteLine("Error constructing ClaimTypesDataSource class: " + ex.Message);

                throw;

            }

        }

 

  • ·         数据源类的实际实现包括为声明类型和唯一声明值添加新项目的方法。其代码非常简单,如下所示:

 

        //add a new item

        public bool AddClaimType(ClaimType newItem)

        {

            bool ret = true;

 

            try

            {

                this.context.AddObject(ClaimTypeDataContext.CLAIM_TYPES_TABLE, newItem);

                this.context.SaveChanges();

            }

            catch (Exception ex)

            {

                Trace.WriteLine("Error adding new claim type: " + ex.Message);

                ret = false;

            }

 

            return ret;

        }

 

唯一声明值数据源的 Add 方法中有一个重要特点需要注意,那就是,如果保存更改时发生异常,它不引发错误或返回 false。那是因为我认为完全有可能是人为错误或其他原因尝试登录多次。一旦我们拥有他们的电子邮件声明记录,任何后续的添加尝试都会引发异常。由于 Azure 不提供强类型的异常,并且我也不想跟踪日志被这些没有意义的麻烦事所充斥,因此我完全没必要担心发生该情况。

  • ·         搜索声明更有趣一些,不过又会涉及一些可以在 LINQ 中做,却不能在 LINQ 中对 Azure 做的事情。我将代码列出如下,并解释我所做的一些选择:

 

        public UniqueClaimValueCollection SearchClaimValues(string ClaimType, string Criteria, int MaxResults)

        {

            UniqueClaimValueCollection results = new UniqueClaimValueCollection();

            UniqueClaimValueCollection returnResults = new UniqueClaimValueCollection();

 

            const int CACHE_TTL = 10;

 

            try

            {

                //look for the current set of claim values in cache

                if (HttpRuntime.Cache[ClaimType] != null)

                    results = (UniqueClaimValueCollection)HttpRuntime.Cache[ClaimType];

                else

                {

                    //not in cache so query Azure

 

                    //Azure doesn't support starts with, so pull all the data for the claim type

                    var values = from UniqueClaimValue cv in this.context.UniqueClaimValues

                                  where cv.PartitionKey == System.Web.HttpUtility.UrlEncode(ClaimType)

                                  select cv;

 

                    //you have to assign it first to actually execute the query and return the results

                    results.UniqueClaimValues = values.ToList();

 

                    //store it in cache

                    HttpRuntime.Cache.Add(ClaimType, results, null,

                        DateTime.Now.AddHours(CACHE_TTL), TimeSpan.Zero,

                        System.Web.Caching.CacheItemPriority.Normal,

                        null);

                }

 

                //now query based on criteria, for the max results

                returnResults.UniqueClaimValues = (from UniqueClaimValue cv in results.UniqueClaimValues

                           where cv.ClaimValue.StartsWith(Criteria)

                           select cv).Take(MaxResults).ToList();

            }

            catch (Exception ex)

            {

                Trace.WriteLine("Error searching claim values: " + ex.Message);

            }

 

            return returnResults;

        }

 

第一件要注意的事情是不能对 Azure 数据使用 StartsWith。这意味着您需要本地检索所有数据,然后使用 StartsWith 表达式。由于检索所有的数据成本太高(实际上是执行表扫描以检索所有行),因此我执行一次该过程然后缓存数据。这样我只需每 10 分钟执行一次“实际”回忆就好。只不过如果在此期间添加了用户,那么将无法在人员选取器中看到这些用户,除非缓存过期并重新检索所有数据。查看结果时,请务必注意这一点。

我实际获取我的数据集之后,便可以执行 StartsWith,还可以限制返回的记录的数量。默认情况下,SharePoint 在人员选取器中显示的记录不会超过 200 条,所以该限制就是我计划在调用此方法时要求的最大数量。但是在这里,我将其作为参数包括进来,以便您可以随意发挥。

队列访问类

坦白地说,这里没有什么特别有意思的事情。只不过是一些从队列添加、读取和删除消息的基本方法。

Azure 辅助角色

辅助角色也没有什么好说的。它每 10 秒钟运行一次,查看队列中是否有任何新的消息。它通过调用队列访问类来执行此过程。如果发现队列中存在任何项目,随即将内容拆分(使用分号分隔)为其组成部分,创建 UniqueClaimValue 类的新实例,然后尝试将该实例添加到唯一声明值表中。执行完该过程后,它从队列删除该消息并移至下一个项目,直至达到其一次所能读取的最大消息数 (32),或者处理完所有的消息。

WCF 应用程序

如之前所述,WCF 应用程序是 SharePoint 代码交流的对象,以便向队列添加项目、获取声明类型列表,以及搜索或解析声明值。与受信任的应用程序一样,它与对其执行调用的 SharePoint 场之间建立了信任。这可以防止请求数据时发生任何形式的标记骗取。此时 WCF 中并没有实现更精细的安全性。出于完整性考虑,WCF 首先在本地 Web 服务器上接受测试,然后移至 Azure 并再次接受测试,以便确认一切都正常工作。

这便是此解决方案的 Azure 组件的基础知识。希望此背景知识能够解释都有哪些移动部件以及它们是如何使用的。在下一部分中,我将讨论 SharePoint 自定义声明提供程序以及如何将这一切拼凑成我们的“完整”Extranet 解决方案。本文随附的文件包含数据访问类、测试项目、Azure 项目、辅助角色和 WCF 项目的所有源代码。同时还提供本文的 Word 文档版本,以便在此网站的布局无法正确显示这些内容时,您能够方便地查看,而不会一头雾水。

这是一篇本地化的博客文章。请访问 The Azure Custom Claim Provider for SharePoint Project Part 2 以查看原文