. В материалах рубрики я буду исходить из того, что читатели, по меньшей мере, знакомы с участниками и потоком коммуникации в модели федеративной безопасности. В этой статье я хочу уделить основное внимание конкретному сценарию, связанному с активной интеграцией с клиентами Windows и службами Windows Communication Foundation (WCF): с кэшированием маркеров и совместным их использованием несколькими посредниками, или proxy, с целью сокращения циклов обращения к службе Security Token Service (STS).
Сценарий кэширования маркеров
Вы неизбежно столкнетесь с такими ситуациями, когда возникает необходимость повторного использования или кэширования маркеров безопасности в клиентах Windows. На рисунке 1 представлена картина сценария, в котором посредник для CustomersService подтверждает права доступа в службе STS и получает маркер, передаваемый в дальнейшем OrdersService и ReportingService.
Рисунок 1. Совместное использование несколькими посредниками маркеров, выданных службой STS |
Цель состоит в том, чтобы при инициализации нескольких посредников для ряда федеративных служб, то есть использующей стороны, или Relying Party (RP), запрос на ввод учетных данных направлялся пользователю не более одного раза. Следует также избегать ненужных обращений к STS с запросами на маркер безопасности для нескольких посредников. Полезно избегать дополнительных обращений к STS и при воссоздании proxy, если время использования канала истекает либо он выходит из строя.
Конкретное содержание утверждений в маркере не относится напрямую к теме статьи, однако стоит отметить, что, поскольку маркеры создаются для определенной использующей стороны (RP), они могут совместно применяться всеми службами RP лишь при соблюдении следующих условий:
- службы RP должны пользоваться одним и тем же сертификатом службы;
- служба STS должна иметь данные, касающиеся всех релевантных адресов RP, используемых в атрибуте AppliesTo маркера Request for Security Token (RST), и обладать возможностью выдачи маркера с утверждениями, которые будут полезны во всех службах использующей стороны;
- когда маркеры выдаются для одной службы RP, они могут быть приняты другой службой лишь в том случае, если эта первая служба допускает применение маркеров, которые могут содержать элемент AudienceUri, указывающий на одну из оставшихся служб RP.
Теперь я расскажу о том, как использовать маркеры для соответствующих посредников. Мы рассмотрим такие вопросы, как обработка таймаутов сеансов и проблема истечения сроков действия маркеров.
ClientCredentials и выданные маркеры
Каждый посредник (канал) экспонирует свойство ClientCredentials, которое используется с целью предоставления данных, необходимых для подтверждения прав доступа к службе. К примеру, перед первым обращением к службе можно указать свойства Windows, UserName или ClientCertificate типа ClientCredentials. Синтаксис указания учетных данных UserName иллюстрируется следующим кодом:
CustomersProxy proxy = new CustomersProxy (); proxy.ClientCredentials.UserName. UserName = this.Username; proxy.ClientCredentials.UserName. Password = this.Password;
При организации федеративной безопасности соответствующие учетные данные Windows, UserName или ClientCertificate устанавливаются в том случае, если предполагается, что оконечные точки STS будут подтверждать полномочия пользователей, обладающих одним из этих типов учетных данных (не будем забывать, что существуют и другие варианты аутентификации, такие как HttpDigest или еще одно свойство IssuedToken от связанной службы STS). Кроме того, тип ClientCredentials экспонирует свойство IssuedToken, которое предоставляет данные об использующей права доступа службе STS, такие как ее адрес и обязательные требования. Эти сведения можно инициализировать программным путем; однако, когда вы генерируете proxy в соответствии с моими разъяснениями, данная информация инициализируется федеративной связывающей конструкцией. На рисунке 2 показано, как запрашивается выданный маркер.
Рисунок 2. Запрос на выданный маркер с помощью IssuedSecurityTokenProvider |
Посредник имеет ссылку на экземпляр ClientCredentials и предоставляет учетные данные пользователя, необходимые для подтверждения прав доступа к STS. Среда выполнения запускает метод CreateSecurityTokenManager () свойства ClientCredentials для создания класса SecurityTokenManager, а точнее — класса ClientCredentialsSecurityTokenManager. Тип SecurityTokenManager отвечает за инициализацию, аутентификацию и сериализацию маркеров. В данном сценарии нам нужно переопределить процедуру инициализации маркеров. Тип SecurityTokenManager содержит реализацию для CreateSecurityTokenProvider (), обеспечивающую формирование соответствующего поставщика маркеров, который берет на себя поставку маркеров для всех исходящих вызовов. В данном случае класс IssuedSecurityTokenProvider используется для запроса выданного маркера от службы STS, указанного в конструкции ClientCredentials. В дальнейшем с помощью этого маркера осуществляется вызов службы.
Свойство IssuedToken конструкции ClientCredentials представляет собой экземпляр типа IssuedTokenClientCredential. Этот тип имеет свойство CachedIssuedTokens, позволяющее управлять кэшированием маркеров. По умолчанию данному свойству задается значение true; это означает, что маркеры будут храниться в кэше до истечения срока их действия и повторно использоваться для обращения к службе, что избавляет от необходимости неоднократной проверки прав доступа к STS. Таким образом снижается уровень непроизводительных расходов для отдельного proxy, но возможности использовать этот кэшированный маркер в нескольких посредниках такой механизм не предоставляет. Для реализации упомянутой возможности вы можете создать пользовательский классс IssuedSecurityTokenProvider, который будет извлекать маркер из кэша общего доступа (если требуемый маркер существует), избегая вызова STS для каждого proxy.
Кэширование выданных маркеров для нескольких посредников
Чтобы создать кэш совместно используемых маркеров на клиенте, вы можете сформировать пользовательский класс IssuedSecurityTokenProvider, который не будет обращаться к службе STS с запросом на маркер, не проверив предварительно локальный кэш маркеров. Для установки настраиваемого класса IssuedSecurityTokenProvider потребуется также создать настраиваемые типы ClientCredentialsSecurityTokenManager и ClientCredentials. Настраиваемый тип ClientCredentials может совместно использоваться различными посредниками, если требования конструкции идентичны, и это приведет оба proxy к одному настраиваемому классу IssuedSecurityTokenProvider, а значит, и к одному кэшированному маркеру, если таковой существует в кэше маркеров. Алгоритм данного сценария показан на рисунке 3.
Рисунок 3. Использование CachedClientCredentials двумя посредниками |
Ниже приводится краткая справка по вновь созданным типам.
- SecurityTokenCache: настраиваемый тип, содержащий ссылку SecurityToken и генерирующий событие в случае изменения маркера, с тем чтобы клиентское приложение могло ответить в случае интереса к такому изменению.
- CachedClientCredentials: настраиваемый тип ClientCredentials, который содержит ссылку на SecurityTokenCache и переопределяет CreateSecurityTokenManager () с целью создания настраиваемого CachedClientCredentialsSecurityTokenManager.
- CachedClientCredentialsSecurityTokenManager: настраиваемый тип ClientCredentialsSecurityTokenManager, который содержит ссылку на SecurityTokenCache и переопределяет CreateSecurityTokenProvider () с целью создания настраиваемого типа CachedIssuedSecurityTokenProvider.
- CachedIssuedSecurityTokenProvider: настраиваемый тип IssuedSecurityTokenProvider, который содержит ссылку на SecurityTokenCache и переопределяет GetTokenCore (), с тем чтобы задать механизм получения выданного маркера посредством возвращения выданного маркера, если он действителен, а если нет — посредством обращения к STS за новым маркером и обновления кэша.
Еще одно возможное представление алгоритма, продемонстрированного на рисунке 3, показано на рисунке 4.
Рисунок 4. Для каждого proxy применяется новая конструкция CachedClientCredentials, при этом по-прежнему используется кэш маркеров |
На мой взгляд, это более целесообразный способ совместного использования кэша маркеров двумя посредниками с помощью одной и той же только что представленной настраиваемой объектной модели. В рассматриваемом случае каждый посредник получает новый экземпляр CachedClientCredentials, CachedClientCredentialsSecurityTokenManager и CachedIssuedSecurityTokenManager и один и тот же экземпляр SecurityTokenCache. Это чуть более чистая реализация, поскольку, по сути, дела различные proxy совместно используют выданный маркер, а не другие настройки конструкции. И хотя они могут быть одними и теми же для всех proxy, обращающихся к группе служб RP, вероятно, было бы правильнее всего вычленять те элементы, которые могут отличаться друг от друга.
В следующем разделе я прокомментирую код, реализующий данный сценарий.
SecurityTokenCache
Механизм создания настраиваемого кэша маркеров безопасности можно представлять по-разному. Возможно, вы работаете с весьма сложным клиентским приложением, которое обращается ко многим группам служб и предполагает кэширование выданных маркеров для каждой группы. А может быть, вам приходится иметь дело с приложением, которое обращается к нескольким связанным службам RP, и каждая из них может использовать выданный маркер. Как показывает мой опыт, второй сценарий встречается чаще, поэтому я позаботилась о том, чтобы реализация оставалась очень простой. В листинге 1 показан код настраиваемого SecurityTokenCache, включающий метод проверки маркера на истечение срока действия.
Данная реализация основывается на следующих предпосылках.
- Клиентское приложение обеспечивает формирование стольких экземпляров SecurityTokenCache, сколько ему необходимо с учетом числа выданных маркеров, требуемых для формирования связанных групп proxy. Обычно речь идет об одном экземпляре.
- Возможно, клиентскому приложению потребуется информация о времени обновления маркера с помощью CachedIssuedSecurityTokenProvider для генерирования события TokenUpdated всякий раз, когда такое обновление происходит.
При формировании этого типа он предоставляется типу CachedClientCredentials.
CachedClientCredentials
Тип CachedClientCredentials обеспечивает формирование настраиваемого класса SecurityTokenManager, который, в свою очередь, формирует настраиваемый класс IssuedSecurityTokenProvider для данного сценария. Рассматриваемая реализация типа CachedClientCredentials показана в листинге 2. Она содержит ссылку на SecurityTokenCache и передает ее, так что после формирования настраиваемого типа CachedClientCredentialsSecurityTokenManager последний имеет к нему доступ. Переопределение CreateSecurityTokenManager () — ключевой элемент данной настраиваемой реализации. Он показан во фрагменте А листинга 2.
Этот клиентский код создаст новый тип CachedClientCredentials и передаст его в исходный тип ClientCredentials для сохранения настроек, инициализированных в ходе конструкции. В листинге 3 представлен код, обеспечивающий формирование кэша маркеров, создание proxy, удаление исходной функции ClientCredentials, создание новой функции CachedClientCredentialsBehavior, передающей кэш маркеров и исходную функцию, и создание учетных данных UserName с целью передачи учетных данных и вызова STS.
Далее я расскажу о том, как организовать совместное использование этого кода различными посредниками и как реагировать на обновление маркеров.
CachedClientCredentials SecurityTokenManager
Как уже отмечалось, тип CachedClientCredentials создает настраиваемый класс SecurityTokenManager,
а именно класс CachedClientCredentialsSecurityTokenManager. Ключевое звено данной реализации — переопределение CreateSecurityTokenProvider (), который возвращает в среду выполнения новый CachedIssuedSecurityTokenProvider. Данная реализация представлена в листинге 4.
Одна из характерных особенностей этой реализации состоит в том, что настраиваемый поставщик создается лишь в том случае, если поставщиком является IssuedSecurityTokenProvide. Отметим, что CachedIssuedSecurityTokenProvider передается (в виде ссылки) экземпляру CachedClientCredentials, чтобы он имел доступ к сохраненному в кэше маркеру.
CachedIssuedSecurityToken Provider
Часть листинга CachedIssuedSecurityTokenProvider представлена в листинге 5. Я опустила «шум», связанный с реализацией ICommunicationObject и IDisposable. Основная задача решается при переопределении GetTokenCore (). Код проверяет кэш на наличие действующего маркера безопасности, и, если таковой имеется, возвращает его. Если же действующий маркер не существует, вызывается базовая функция по считыванию, и кэш маркеров обновляется. Маркер считается действующим, если значение его свойства ValidTo больше текущего времени UTC.
Совместное использование CachedClientCredentials и обновление клиентских утверждений
Рисунок 4 иллюстрирует ситуацию, когда каждый proxy имеет собственную ссылку CachedClientCredentials, но все они используют один и тот же SecurityTokenCache. В листинге 6 показан код, позволяющий создать такую ситуацию через инициализацию каждого экземпляра CachedClientCredentials с помощью одного и того же экземпляра SecurityTokenCache. Кроме того, этот код иллюстрирует сбор утверждений о клиентах и подключение события TokenUpdated, с тем чтобы утверждения о клиентах обновлялись при считывании нового маркера.
При первоначальной загрузке приложения и инициализации каждого proxy первый proxy, который попытается осуществить вызов, получит маркер и введет информацию в кэш для последующего использования совместно с другим proxy.
Время жизни сеансов и маркеров
Я уже рассказывала о том, как организовать совместное использование маркера на клиенте с несколькими proxy, и предложила свой подход к проблеме синхронизации утверждений о клиентской стороне с новейшим маркером. Но есть еще один вопрос, который может представлять сложность при работе со сценариями федеративной безопасности; речь идет о тайм-ауте защищенного сеанса и о поврежденных каналах.
Всякий раз, когда существует транспортный сеанс, он переводится в режим ожидания на сервере в период неактивности канала. Кроме того, любое не перехваченное исключение, попадающее в службу, вызывает отказ канала службы, в результате чего использование proxy становится невозможным. Любопытно, что пользователю необязательно знать о тайм-аутах или исключениях при подключении — он может предпочесть, чтобы приложение создало новый канал, обеспечивающий продолжение работы с приложением. Как видно из реализации, описанной выше, при наличии недействительного маркера будет произведено еще одно обращение к STS с целью получения вновь выданного маркера. Но что будет происходить в двух других сценариях?
Если говорить о тайм-ауте сеанса, клиент, возможно, не будет иметь о нем представления до тех пор, пока не попытается обратиться к службе, — в этот момент обращение завершится с ошибкой. Поскольку удостовериться, что ошибка вызова была связана с тайм-аутом, возможности нет, нам нужно, чтобы попытка вызова была повторена после воссоздания proxy. Если повторная попытка завершается неудачей, значит, мы имеем дело с более серьезной проблемой, связанной с передачей данных. Но если попытка успешна, выдается новый маркер, кэш обновляется, обновляются утверждения о клиентах, пользовательский интерфейс восстанавливается, и оказывается, что все прекрасно в этом лучшем из миров.
Если исключение создается службой и это исключение не является исключением CommunicationException, пользователю необходимо предъявить сообщение об ошибке, но следующая попытка использовать proxy закончится неудачей, так как канал находится в аварийном состоянии. Пользователю необязательно знать об этом, так как его уже проинформировали об инициирующем исключении, которое вызвало сбой в работе канала. Итак, при сбое в работе канала нам необходимо воссоздать proxy, и тогда следующий вызов будет выполнен надлежащим образом. И опять все к лучшему в этом лучшем из миров.
Данный вопрос рассматривается здесь лишь в самом общем виде, поскольку я описала его более подробно в отдельной «белой книге» и в нескольких кратких веб-трансляциях, опубликованных по адресу http://wcfguidanceforwpf.codeplex.com. Кроме того, я написала программу — генератор proxy ExceptionHandlingWCFProxyGenerator (http://wcfproxygenerator.codeplex.com), автоматически создающую специальный базовый класс proxy, который решает упомянутые вопросы и дает возможность оградить пользователя от ненужных исключений. Дополнительное достоинство состоит в том, что тот же proxy помогает нам воссоздавать и получать вновь выданный маркер в случае сбоя канала.
Поддержка сценариев CardSpace
Мне пришлось написать дополнение к типу ClientCredentials, чтобы обеспечить поддержку кэширования выданных маркеров при выполнении сценариев CardSpace. Я предусмотрела переопределение GetInfoCardSecurityToken (), ибо в случаях, когда proxy конфигурируется для вызова CardSpace, этот компонент вызывается вместо IssuedSecurityTokenProvider. Реализация представлена в листинге 7.
Кэширование маркеров: полезный прием
Я остановилась на теме кэширования выданных маркеров для сценариев, в которых будет задействовано несколько proxy из клиентских приложений Windows в сценарии с использованием федеративной схемы организации безопасности. Используя метод кэширования маркеров в ситуациях, где это возможно, вы избежите ненужных обращений к STS и сможете применять более эффективный механизм аутентификации. Более подробные сведения относительно средств клиентов Windows, связанных с обеспечением безопасности на основе утверждений и федеративной безопасности, можно найти на claimsbasedwpf.codeplex.com.
Ресурсы для данной статьи размещены по адресу: www.dasblonde.net/downloads/wif/cachingissuedtokens.zip.
Мишель Бустаманте (mlb@idesign.net) — главный архитектор компании IDesign, региональный директор Microsoft в Сан-Диего и обладатель сертификата Microsoft MVP for Connected Systems. Ведет блог по адресу www.dasblonde.net
public class SecurityTokenCache { private SecurityToken _Token; public SecurityToken Token { get { return _Token; } set { _Token = value; if (TokenUpdated != null) { TokenUpdated(this, null); } } } public bool IsValidToken() { if (this._Token == null) return false; return (DateTime.UtcNow <= this._Token.ValidTo. ToUniversalTime()); } public event EventHandler TokenUpdated; }
public class CachedClientCredentials: ClientCredentials { public SecurityTokenCache TokenCache { get; private set; } public CachedClientCredentials(SecurityTokenCache tokenCache): base() { this.TokenCache = tokenCache; } public CachedClientCredentials(SecurityTokenCache tokenCache, ClientCredentials clientCredentials) : base(clientCredentials) { this.TokenCache = tokenCache; } public CachedClientCredentials(CachedClientCredentials clientCredentials): base(clientCredentials) { this.TokenCache = clientCredentials.TokenCache; } public override System.IdentityModel.Selectors.Security- TokenManager CreateSecurityTokenManager() { Начало фрагмента А return new CachedClientCredentialsSecurityTokenManager ((CachedClientCredentials)this.Clone()); Конец фрагмента А } protected override ClientCredentials CloneCore() { return new CachedClientCredentials(this); } }
this.TokenCache = new SecurityTokenCache(); this._Proxy = new CustomersServiceProxy(); ClientCredentials oldCreds = this._Proxy.Endpoint.Behaviors. Remove(); CachedClientCredentials newCreds = new CachedClientCredentials (this.TokenCache, oldCreds); this._Proxy.Endpoint.Behaviors.Add(newCreds); this._Proxy.ClientCredentials.UserName.UserName = this.Username; this._Proxy.ClientCredentials.UserName.Password = this.Password; this._Proxy.Open();
public class CachedClientCredentialsSecurityTokenManager : ClientCredentialsSecurityTokenManager { public CachedClientCredentialsSecurityTokenManager (CachedClientCredentials clientCredentials): base(clientCredentials) { } public override System.IdentityModel.Selectors.Security- TokenProvider CreateSecurityTokenProvider(System.IdentityModel.Selectors. SecurityTokenRequirement tokenRequirement) { IssuedSecurityTokenProvider provider = base.CreateSecurityTokenProvider(tokenRequirement) as IssuedSecurityTokenProvider; if (provider == null) return base.CreateSecurityTokenProvider (tokenRequirement); CachedIssuedSecurityTokenProvider cachedProvider = new CachedIssuedSecurityTokenProvider(provider, (CachedClientCredentials)this.ClientCredentials); return cachedProvider; } }
public class CachedIssuedSecurityTokenProvider: IssuedSecurityTokenProvider, ICommunicationObject, IDisposable { private CachedClientCredentials ClientCredentials { get; set; } private IssuedSecurityTokenProvider InnerProvider {get; set;} public CachedIssuedSecurityTokenProvider (IssuedSecurityTokenProvider provider, CachedClientCredentials clientCredentials):base() { this.InnerProvider = provider; this.ClientCredentials = clientCredentials; this.CacheIssuedTokens = provider.CacheIssuedTokens; this.IdentityVerifier = provider.IdentityVerifier; this.IssuedTokenRenewalThresholdPercentage = provider.IssuedTokenRenewalThresholdPercentage; this.IssuerAddress = provider.IssuerAddress; this.IssuerBinding = provider.IssuerBinding; foreach (IEndpointBehavior item in provider.IssuerChannelBehaviors) this.IssuerChannelBehaviors.Add(item); this.KeyEntropyMode = provider.KeyEntropyMode; this.MaxIssuedTokenCachingTime = provider.MaxIssuedTokenCachingTime; this.MessageSecurityVersion = provider.MessageSecurityVersion; this.SecurityAlgorithmSuite = provider.SecurityAlgorithmSuite; this.SecurityTokenSerializer = provider.SecurityTokenSerializer; this.TargetAddress = provider.TargetAddress; foreach (XmlElement item in provider.TokenRequestParameters) this.TokenRequestParameters.Add(item); } protected override System.IdentityModel.Tokens. SecurityToken GetTokenCore(TimeSpan timeout) { SecurityToken securityToken = null; if (this.ClientCredentials.TokenCache.IsValidToken()) { securityToken = this.ClientCredentials. TokenCache.Token; } else { securityToken = this.InnerProvider.GetToken (timeout); this.ClientCredentials.TokenCache.Token = securityToken; } return securityToken; } }
this.TokenCache = new SecurityTokenCache(); customersProxy = new CustomersServiceProxy(); ClientCredentials oldCreds = customersProxy.Endpoint. Behaviors.Remove(); CachedClientCredentials newCreds = new CachedClientCredentials (this.TokenCache, oldCreds); customersProxy.Endpoint.Behaviors.Add(newCreds); customersProxy.ClientCredentials.UserName.UserName = this.Username; customersProxy.ClientCredentials.UserName.Password = this.Password; ordersProxy = new OrdersServiceProxy(); ClientCredentials oldCreds = ordersProxy.Endpoint.Behaviors. Remove (); newCreds = new CachedClientCredentials(this.TokenCache, oldCreds); ordersProxy.Endpoint.Behaviors.Add(newCreds); ordersProxy.ClientCredentials.UserName.UserName = this.Username; ordersProxy.ClientCredentials.UserName.Password = this.Password;
protected override System.IdentityModel.Tokens.SecurityToken GetInfoCardSecurityToken(bool requiresInfoCard, CardSpacePolicyElement[] chain, SecurityTokenSerializer tokenSerializer) { SecurityToken securityToken = null; if (this.TokenCache.IsValidToken()) { securityToken = this.TokenCache.Token; } else { try { securityToken = base.GetInfoCardSecurity- Token(requiresInfoCard, chain, tokenSerializer); this.TokenCache.Token = securityToken; } catch (UserCancellationException cancelEx) { throw new Exception(«Login cancelled by user.», cancelEx); } } return securityToken; }