Настраиваемый поставщик утверждений Azure для проекта SharePoint, часть 2

Исходная статья опубликована в среду, 15 февраля 2012 г.

В первой части этой серии я в общих чертах описал цель данного проекта, которая на высоком уровне заключается в использовании хранилища таблиц Windows Azure в качестве хранилища данных настраиваемого поставщика утверждений SharePoint. Поставщик утверждений будет использовать набор CASI Kit для извлечения из Windows Azure данных, необходимых для обеспечения работы средства выбора людей (т. е. адресной книги) и разрешения имен в поле ввода. 

В третьей части я создам все компоненты, используемые в ферме SharePoint. К ним относится пользовательский компонент, основанный на CASI Kit, который управляет обменом данными между SharePoint и Azure. Существует настраиваемая веб-часть, которая собирает сведения о новых пользователях и отправляет их в очередь Azure. Наконец, существует настраиваемый поставщик утверждений, который обменивается данными с хранилищем таблиц Azure через WCF — через пользовательский компонент CASI Kit — для поддержки функциональных возможностей элемента управления вводом и средства выбора людей.

Давайте остановимся на этом сценарии более подробно.

Этот тип решения очень хорошо вписывается в довольно распространенный сценарий, который заключается в использовании минимально управляемой экстрасети. Например, вам нужно, чтобы партнеры или клиенты могли войти на ваш веб-сайт, запросить учетную запись и автоматически "подготовить" эту учетную запись ("подготовка" может означать множество разных действий для разных пользователей). Мы будем использовать этот сценарий в качестве базового, но, конечно, позволим ресурсам общедоступного облака выполнить некоторые действия за нас.

Начнем с обзора компонентов облака, которые мы будем разрабатывать самостоятельно:

  • Таблица для отслеживания всех типов утверждений, которые мы собираемся поддерживать
  • Таблица для отслеживания всех уникальных значений утверждений для средства выбора людей
  • Очередь, в которую мы можем отправлять данные, добавляемые в список уникальных значений утверждений
  • Несколько классов доступа к данным для чтения и записи данных из таблиц 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, http://www.foo.com/blah, и т. д. Если тип утверждения содержит знаки "косая черта", Azure не сможет сохранить PartitionKey с такими значениями. Поэтому мы закодируем их в удобный формат, поддерживаемый Azure. Как я упоминал выше, в нашем случае используется утверждение адреса электронной почты, поэтому типом утверждения будет http://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. В принципе, можно выполнить объединение в LINQ, однако при работе с данными Azure возможности LINQ настолько ограничены, что мне кажется проще использовать денормализацию. Если у вас есть другие соображения на этот счет, напишите о них в комментариях. Мне было бы интересно узнать, что вы думаете. Итак, в нашем случае отображаемым именем будет "Email", поскольку это тип утверждения, который сохраняется в этом классе.

Очередь утверждений

Принцип очереди утверждений довольно прост — в этой очереди мы будем сохранять запросы на "новых пользователей", а затем рабочий процесс Azure будет считывать их из очереди и пересылать данные в таблицу уникальных значений утверждений. Основная причина использования такого подхода заключается в том, что работа с хранилищем таблиц Azure может быть довольно медленной, а отправка элементов в очередь выполняется очень быстро. Выбрав этот подход, мы сможем минимизировать влияние на веб-сайт SharePoint.

Классы доступа к данным

Одним из обычных аспектов работы с хранилищем таблиц Azure и очередями является необходимость создания собственного класса доступа к данным. Для хранилища таблиц необходимо создать класс контекста данных и класс источника данных. Я не буду подробно останавливаться на этом вопросе, поскольку эти сведения можно найти в Интернете; кроме того, к этой статье я прилагаю исходный код для проекта Azure, из которого вы также сможете получить всю необходимую информацию. 

Здесь хотелось бы сделать одно важное замечание по поводу персонального выбора стиля. Я предпочитаю выделять код доступа к данным Azure в отдельный проект. Это позволяет скомпилировать его в отдельную сборку и использовать даже из проектов, не имеющих отношения к Azure. Например, в представленном здесь примере кода вы найдете приложение форм Windows, которое я использовал для тестирования серверных компонентов Azure. Оно не предназначено для работы исключительно с Azure, а лишь ссылается на некоторые сборки Azure и на мою сборку доступа к данным. Я могу использовать свою сборку как в этом приложении, так и в проекте WCF, который я использую в качестве интерфейса доступа к данным для SharePoint.

Вот некоторые особенности классов доступа к данным:

  • ·         У меня есть отдельный класс "контейнера" для данных, которые я собираюсь возвращать, — типов утверждений и уникальных значений утверждений. Под классом контейнера я подразумеваю простой класс с общим свойством типа "список"<>. При запросе данных я возвращаю этот класс, а не просто списокlt;> результатов. Я поступаю так потому, что при возврате списка<> из Azure клиент получает только последний элемент списка (если сделать то же самое из локально размещенного 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>();

        }

    }

}

 

 

  • ·         Классы контекста данных очень просты — ничего гениального, как сказала бы моя подруга Веса. Вот как выглядит такой класс:

 

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

            {

                //здесь вы настраиваете имя таблицы в хранилище таблиц Azure,

                //с которым вы собираетесь работать

                return this.CreateQuery<ClaimType>(CLAIM_TYPES_TABLE);

            }

        }

 

    }

}

 

  • ·         В классах источника данных я применяю немного другой подход для подключения к Azure. В большинстве примеров, которые я видел в Интернете, для считывания учетных данных используется класс параметров регистрации (это неточное название, точное я не помню). В данном случае проблема заключается в том, что я не ограничиваюсь контекстом Azure, так как хочу, чтобы мой класс данных работал и вне Azure. Поэтому я просто создаю в свойствах проекта параметр, который будет содержать имя учетной записи и ключ, необходимый для подключения к моей учетной записи Azure. Поэтому в обоих классах источника данных для создания подключения к хранилищу Azure используется следующий код:

 

        private static CloudStorageAccount storageAccount;

        private ClaimTypeDataContext context;

 

 

        //статический конструктор, поэтому активируется только один раз

        static ClaimTypesDataSource()

        {

            try

            {

                //получение данных о подключении к учетной записи хранилища

                string storeCon = Properties.Settings.Default.StorageAccount;

 

                //извлечение данных учетной записи

                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;

            }

        }

 

 

        //новый конструктор

        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;

            }

        }

 

  • ·         Фактическая реализация классов источника данных включает метод для добавления нового элемента как для типа утверждения, так и для уникального значения утверждения. Это очень простой код, который выглядит следующим образом:

 

        //добавление нового элемента

        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, но не при работе с данными Azure. Ниже я приведу код, а затем объясню некоторые его особенности:

 

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

        {

            UniqueClaimValueCollection results = new UniqueClaimValueCollection();

            UniqueClaimValueCollection returnResults = new UniqueClaimValueCollection();

 

            const int CACHE_TTL = 10;

 

            try

            {

                //поиск текущего набора значений утверждений в кэше

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

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

                else

                {

                    //данные в кэше не найдены, поэтому запрашиваем Azure

 

                    //Azure не поддерживает выражение starts with, поэтому извлекаем все данные для типа утверждения

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

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

                                  select cv;

 

                    //сначала нужно присвоить значение, чтобы фактически выполнить запрос и вернуть результаты

                    results.UniqueClaimValues = values.ToList();

 

                    //сохраняем в кэше

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

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

                        System.Web.Caching.CacheItemPriority.Normal,

                        null);

                }

 

                //теперь запрос основан на критериях, для получения максимальных результатов

                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 сначала было протестировано на локальном веб-сервере, а затем перенесено в Azure, где было протестировано повторно, чтобы убедиться, что все работает.

Итак, вот основные компоненты данного решения Azure. Надеюсь, мне удалось разъяснить их сущность и назначение. В следующей части мы рассмотрим настраиваемого поставщика утверждений SharePoint, а также соберем все компоненты в единое целое и получим готовое решение экстрасети. В файлах, приложенных к этой статье, содержатся все исходные коды для класса доступа к данным, тестового проекта, проекта Azure, проектов рабочей роли и WCF. В одном из файлов представлена копия этой статьи в виде документа Word, содержимое которого выглядит в соответствии с моим замыслом, безнадежно искаженным визуализацией на этом сайте.

Это локализованная запись блога. Исходная статья доступна по адресу The Azure Custom Claim Provider for SharePoint Project Part 2