在 SharePoint 專案中使用 Azure 自訂宣告提供者,第 2 部

英文原文已於 2012 年 2 月 15 日星期三發佈

在本系列的第 1 部 中,我簡要說明了本專案的宗旨,大體上是要使用 Windows Azure 資料表儲存體作為 SharePoint 自訂宣告提供者的資料存放區。此宣告提供者將會使用 CASI 套件 (可能為英文網頁)從 Windows Azure 擷取所需的資料,以提供人員選擇 (例如通訊錄) 及輸入控制項名稱解析功能。

第 3 部中,我將會建立所有用於 SharePoint 伺服器陣列中的元件。它包含有以 CASI 套件為基礎,負責管理所有 SharePoint 與 Azure 之間通訊的自訂元件。我們還會提到一個自訂網頁組件,它可以擷取有關新使用者的資訊,並將其插入 Azure 佇列。最後,還有一種自訂宣告提供者,會透過 WCF (再經由 CASI 套年自訂元件) 與 Azure 資料表儲存體交流,以啟用輸入控制項與人員選擇功能。

現在讓我們更深入探討此案例。

這種解決方法類型對於大部份常見情況非常管用,也就是當您想建立一個管理最少的外部網路時。舉例來說,您希望您的合作夥伴或客戶能點擊您的網站,索取帳號,接著能夠自動「佈建」該帳號 (雖然許多人對「佈建」這個詞有不同的見解)。我們就用它當作案例基礎;當然,也要讓我們的公用雲端資源發揮一些作用。

首先就來看看我們要自行開發的一些雲端元件:

  • 一份記載所有我們要支援的宣告類型之資料表
  • 另一份記載用於人員選擇的所有唯一宣告值之資料表
  • 用以接收我們所傳送而應加入唯一宣告值清單之資料的佇列
  • 用以讀寫 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 可以協助您找到解答。現在值得特別指出的一點是,我會將使用 URL 編碼要為 PartitionKey 儲存的。這是因為在此案例中,我的 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 使用 URL 編碼的值,因為它會是宣告類型,其中含有斜線。其次,如我在使用 Azure 資料表儲存體時常見到的,資料會經過反正規化處理,因為這裡不像在 SQL 中有 JOIN 的概念。技術上來說,您可以在 LINQ 中使用 JOIN,但在處理 Azure 資料時,LINQ 中有許多東西都無法使用或效果不彰,我發現還不如就任它反正規化還比較簡單。如果各位有其他的見解,歡迎留言,我很想聽聽大家的想法。所以呢,在我們的例子裡,顯示名稱會是 “Email”,因為那是我們儲存在此類別中的宣告類型。

宣告佇列

宣告佇列十分直接。我們會儲存在該佇列中尋找「新使用者」的要求,然後 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;

        }

 

要注意的第一件事是您不能使用 StartsWith 來處理 Azure 資料。因此這代表您必須從本機擷取所有資料,再使用 StartsWith 運算式。鑑於擷取所有資料可能會是一項龐大的作業 (它實際上是掃描資料表一遍,以擷取所有列項),我只做一次,然後將資料存入快取中。這麼一來,我只要每 10 分鐘「真正」重新叫用一次即可。缺點是如果在那段時間內新增使用者的話,要直到快取過期然後再次重新擷取所有資料以後,才能在人員選擇中看到他們。當您查看結果時,請務必記住這一點。

待我真正取得資料集之後就可以使用 StartsWith,也可以限制要傳回的記錄數目了。SharePoint 預設並不會顯示 200 筆以上的人員選擇記錄,所以當我呼叫此方法時,那就是我預計要求的數目上限。但我在這裡是讓它作為一個參數,所以您可以隨意修改。

佇列存取類別

老實說這裡真的沒什麼特別有意思的東西。只有一些基本方法,用來新增、讀取及刪除佇列中的訊息。

Azure 工作角色

工作角色一樣也沒什麼特別之處。它每 10 秒鐘會醒來一次,看看佇列中有沒有新訊息。它的作法是呼叫佇列存取類別。如果在其中找到任何項目,它就會用分號將內容切分為幾個本身的組成部分,建立一個 UniqueClaimValue 類別的新執行個體,然後嘗試將該執行個體加入唯一宣告值資料表中。完成之後,它就會從佇列刪除該訊息,並移至下一個項目,直到它達到單次讀取訊息的數量上限 (32),或沒有剩下任何訊息為止。

WCF 應用程式

如之前所述,WCF 應用程式是 SharePoint 程式碼所交談的對象,以新增項目到佇列、取得宣告類型清單、搜尋或解析宣告值等動作。就像所有受信任的應用程式一樣,它和呼叫它的 SharePoint 伺服器陣列之間會建立信任關係。如此可避免要求資料時任何種類的 Token 詐騙。到目前為止,WCF 本身尚未建置任何精細的安全防護。WCF 的完整性會先在本機網頁瀏覽器中進行測試,然後移到 Azure 中會再次測試,以確認一切正常。

以上就是本解決方案之 Azure 元件的基本概念。希望這份背景資料能協助您認識各個部件與其用法。在下一部分中,我們將會討論 SharePoint 自訂宣告提供者,以及如何統合每個環節,打造一個「五臟俱全」的外部網路解決方案。本文匠附的檔案中包含了所有用於資料存取類別、測試專案、Azure 專案、工作角色及 WCF 專案的原始程式碼。同時還附帶一份本文的 Word 文件。若此網頁的排版格式不良,煩請參閱 Word 文件,免得您看得一頭霧水囉。

這是翻譯後的部落格文章。英文原文請參閱 The Azure Custom Claim Provider for SharePoint Project Part 2