Битва За Adfs (Службы Федерации Active Directory)



Фон Проект начинался как портал на базе SP 2007, а затем на базе SP 2010. Изначально все пользователи находились в Active Directory. Был только один тип пользователей.

Связи между ними были довольно простыми.

Появились новые типы пользователей, которые стали связаны друг с другом сложными способами.

Также проект постепенно обзавелся различными сопутствующими подсистемами, часть из которых работала внутри портала, часть — вне его.

И это еще больше усложнило схему авторизации.



Битва за ADFS (службы федерации Active Directory)



Какие проблемы?

До определенного момента использование NTLM удовлетворяло все потребности.

Единственная проблема заключалась в том, что мы столкнулись с тем, что при переходе на связанный сервис, если url-адрес отличался от адреса портала, нам приходилось заново вводить логин и пароль.

В принципе, эту проблему можно решить с помощью прокси-продукта веб-приложения.

Однако позже появились модули Java, и поэтому среда стала во многом гетерогенной.

Также на горизонте стоит необходимость предоставления доступа пользователям со сторонних доменов.

Для решения этих проблем стала очевидной необходимость в системе единого входа (SSO).

Было принято решение внедрить ADFS и перевести портал и все сервисы на эту технологию.

Мы обсудили наше видение с заказчиком и запланировали день X, когда все должно заработать на ADFS.

Ход событий:



День Х – 1 год
Первым делом мы решили перевести один из модулей на ADFS. Идея заключалась в том, чтобы включить ADFS на портале, ударить по всем шишкам, которые можно заполнить, сгореть там, где можно сгореть и т. д. Столкнувшись с определенным количеством проблем, мы успешно осуществили это переключение, о котором уже писали.

habrahabr.ru/company/eastbanctech/blog/209834 .



День Х – 3 месяца
Первое, с чего мы начали, — это рефакторинг кода, чтобы он корректно работал при аутентификации по утверждениям.

Несколько примеров того, что мы изменили: 1. Распространение разрешений на элементы списка SharePoint для пользователей и групп Active Directory. Поскольку разрешения теперь должны были раздаваться за марки, все эти места пришлось переписать.

Хорошо, что почти все такие места использовали одну из наших библиотек для работы с AD (а те, которые не использовали, мы изменили, чтобы они тоже работали с нашей библиотекой).

Вместо выдачи разрешений пользователям типа «домен\пользователь», разрешения стали выдаваться брендам «i:0e.t|ADFS|user@domain».

Для «домен\группа» — брендам вида «c:0-.

t|ADFS|group» соответственно.

Нам также пришлось отделить случаи, когда разрешения предоставляются пользователю, от случаев, когда они выдаются группе, поскольку без использования утверждений группы домена выглядят в MS SharePoint одинаково.

Таким образом метод GetPrincipalName, определяющий полное имя принципала, я превратил в 2:

  
  
  
  
  
   

public static string GetGroupPrincipalName(string group) { if (string.IsNullOrEmpty(TrustedIdentityProviderName)) { return string.Format(CultureInfo.InvariantCulture, "{0}\\{1}", CurrentDomain, GetPrincipalNameWithoutDomain(group)); } return string.Concat("c:0-.

t|", TrustedIdentityProviderName, "|", GetPrincipalNameWithoutDomain(@group)); } public static string GetUserPrincipalName(string user) { if (string.IsNullOrEmpty(TrustedIdentityProviderName)) { return string.Format(CultureInfo.InvariantCulture, "{0}\\{1}", CurrentDomain, GetPrincipalNameWithoutDomain(user.ToLower(CultureInfo.InvariantCulture))); } return string.Concat("i:0e.t|", TrustedIdentityProviderName, "|", GetPrincipalNameWithoutDomain(user), TrustedIdentityProviderDomain); }

2. Проверка членства в группах Проверки членства в группах также проходили через эту библиотеку, поэтому не было необходимости их слишком сильно менять.

Мы изменили только функциональность, определенную пользователем.

Теперь на вход можно подавать как доменных пользователей, так и брендовых:

public static string GetPrincipalNameWithoutDomain(string principal) { if (string.IsNullOrEmpty(principal)) return string.Empty; return Regex.Match(principal.Trim(), @"(i:)?0e.t.*\|(?<userName>[\d\w_&\.

\s-]+)@[\w\d\.

_]|(c:)?0-.

t.*\|(?<userName>[\d\w_&\.

\s-]+)$|^(?<userName>[\d\w_&\.

\s-]+)@[\w\d\.

_]|^(?<userName>[\d\w_&\.

\s-]+)$|[\w]+\\(?<userName>[\d\w_&\.

\s-]+)$") .

Groups["userName"].

Value; }

Почему это было сделано? В какой-то момент у нас получилось так, что часть команды работала на серверах, конвертированных в ADFS, а остальная часть — на серверах с NTLM-аутентификацией.

Поскольку нам удалось добиться того, чтобы наша библиотека работала одинаково и для NTLM, и для ADFS, это помогло не останавливать разработку новых модулей.



День Х – 2 месяца
Через месяц-два портал ожил, заработали базовые вещи, новостной модуль, бизнес-процессы на базе Sharepoint Workflows и многое другое.

После этого мы перенесли портал в тестовую среду, составили чек-лист для наших бесконечных модулей, назначили ответственных за каждый модуль и полетели по схеме: • модуль изготовлен • развернуто для тестирования • нашел кучу ошибок и исправил их • модуль готов

По пути мы столкнулись с парочкой интересных проблем.



1. Первой проблемой, с которой мы столкнулись, был вызов SOAP-сервисов.

Большинство сервисов wcf, которые «живут» в папке _vti_bin и используются скриптами из браузера и через webHttpBinding. Из остальных сервисов часть сервисов используется для межмодульного взаимодействия, а остальные нужны другим клиентским системам, с которыми настроена интеграция.

Естественно, большая часть этих взаимодействий основана на протоколе SOAP (это удобнее) и завязано на NTLM-аутентификации.

Для начала мы попытались понять, насколько проблематично будет перевести всех клиентов (по крайней мере, наших, работающих под WCF) на ADFS. Мы попробовали, ужаснулись количеству необходимых движений и сложности настройки клиента и забыли эту идею.

Потратил некоторое время, пытаясь заставить SharePoint работать с двумя схемами аутентификации для служб одновременно (незаметно для пользователей).

Не получилось, NTLM упорно отказывался работать (о причинах ниже).

Таким образом, нам срочно потребовалось вернуть «настоящий» NTLM для сервисов.

Поэтому мы обратились к такой возможности SharePoint, как расширение веб-приложений.

Мы рассудили, что REST-сервисы для браузера останутся по основному адресу, аутентификацией которого будет заниматься браузер пользователя, а все сервисные сервисы переместятся на адрес расширенного сайта, который будет работать с NTLM. Казалось бы, все просто.

Начнем.

Мы расширили уже перенесенный на ADFS портал с помощью стандартных инструментов SharePoint (Extend Web application), выбрав NTLM в качестве поставщика аутентификации для расширенного сайта.

Ожидаемый результат: все пользователи сервисов WCF из каталога сервисов «ISAPI» в SharePoint работают с ними как и раньше, максимум редактируют адрес вызываемого сервиса в разделе клиента в файле конфигурации «Web.config».

Расширение основного сайта, который уже был переведен на ADFS, не сразу решило проблемы вызова WCF-сервисов для пользователей через NTLM-аутентификацию — на каждый вызов WCF-сервиса мы неумолимо получали ответ: «HTTP-запрос неавторизован клиентом».

схема аутентификации «Ntlm».

Заголовок аутентификации, полученный от сервера, был «Negotiate,NTLM».

Реальная причина проблемы заключалась в том, что при расширении основного сайта в основном файле конфигурации «Web.config» Sharepoint скопировал неправильные модули аутентификации в раздел «модули».

:

<add name="FederatedAuthentication" type="Microsoft.SharePoint.IdentityModel.SPFederationAuthenticationModule …" /> <add name="SessionAuthentication" type="Microsoft.SharePoint.IdentityModel.SPSessionAuthenticationModule …" /> <add name="SPWindowsClaimsAuthentication" type="Microsoft.SharePoint.IdentityModel.SPWindowsClaimsAuthenticationHttpModule …" />

Тогда как правильный модуль для сайта NTLM:

<add name="Session" type="System.Web.SessionState.SessionStateModule" />

После долгих обсуждений мы решили действовать в следующей последовательности: — создайте расширение основного сайта перед переносом его в ADFS, тогда останется тот же правильный модуль NTLM-аутентификации; — перенести основной сайт портала в Sharepoint в ADFS Если у вас есть одинаковые службы WCF в ISAPI, которые одновременно имеют конечные точки для доступа и через NTLM, и через ADFS, то они потребуют одновременной поддержки сайтом IIS как «Аутентификации по формам», так и «Аутентификации Windows».

В нашем случае мы имеем основной сайт Sharepoint и его расширение, которые намеренно не поддерживают оба метода аутентификации одновременно.

Для решения этой проблемы мы использовали: — Создайте две подпапки в ISAPI «ModuleServiceAdfs», «ModuleServiceNtlm».

— скопируйте svc-файл службы WCF в обе папки — создайте в каждой папке свой файл конфигурации «Web.config» — в первой для ADFS, во второй для NTLM.

2. Вторая проблема — устаревание токена saml.

Большинство запросов к серверу выполнялись через ajax с использованием jquery. При этом периодически возникают ситуации, когда самл-токен становится недействительным (когда токен устарел, при перезапуске пула SharePoint, при перезапуске пула ADFS).

Стандартный механизм перенаправления на страницу аутентификации с автообновлением токена или вводом логина/пароля и последующим возвратом на исходную страницу не работает в случае jquery.ajax. Да и сама ситуация, когда пользователя приходится отправлять на страницу аутентификации только для того, чтобы автоматически вернуть его на исходную страницу, но с потерянными результатами его работы, энтузиазма не внушала.

Быстрый поиск в Интернете привел нас к следующему: это решение .

Для тех, кому лень идти по ссылке, суть решения — создать страницу preauth и обернуть все Ajax-запросы обработчиком, который в случае 401 ответа от сервера загружает эту страницу через iFrame, а затем повторяет исходный запрос.

Создание единой точки для выполнения ajax-запросов в нашем случае не составило проблемы, так как приложение было написано таким образом (О нашем подходе мы уже рассказывали в этот И этот статьи).



Мы развили найденное решение, внеся некоторые дополнения:

1. В случае, если подряд было сделано несколько запросов, то вместо создания iFrame для каждого при получении 401, для второго и последующих запросов возвращаем тот же deferred, что был создан для первого запроса.

2. Этот подход работает в случае, когда токен устарел или Sharepoint его «забыл».

Но это не сработало в случае, когда ADFS нас «забыла» — в этом случае нам нужно заново ввести логин/пароль.

Описанный в статье подход в таких случаях приводил к бесконечному циклу загрузки страницы preauth через iFrame без какого-либо результата.

Прямой редирект на страницу аутентификации тоже был не очень приятен, так как означал потерю работы пользователя.

Решением стало отображение модала для ввода логина/пароля на тот случай, если загрузка через iFrame не помогла, и мы снова получили 401. Модал в свою очередь делает вызов кастомного сервиса, который уже выполняет аутентификацию в ADFS. После завершения аутентификации мы дублируем исходный запрос/запросы ajax. Расширенный код выглядит так:

refreshToken: function () { if (wcfDispatcherDef.frameLoadPromise === undefined) { return jquery.Deferred(function (d) { wcfDispatcherDef.frameLoadPromise = d; var iFrame = jquery('<iframe></iframe>'); iFrame.hide(); iFrame.appendTo('body'); iFrame.attr('src', wcfDispatcherDef.PreauthUrl); iFrame.load(function () { setTimeout(function () { wcfDispatcherDef.frameLoadPromise = undefined; d.resolve(); iFrame.remove(); }, 100); }); }); } else { return wcfDispatcherDef.frameLoadPromise; } }, makeServiceCall: function (settings, initialPromise) { var self = this; var d = initialPromise || jquery.Deferred(); var promise = jquery.ajax(settings) .

done(function () { d.resolveWith(self.requestContext || self, jquery.makeArray(arguments)); }).

fail(function (error) { if (error.status * 1 === ETR.HttpStatusCode.Unauthorized && wcfDispatcherDef.HandleUnauthorizedError === true) { if (initialPromise) { wcfDispatcherDef.AuthDialog.show().

done(function (result) { if (result === true) { self.makeServiceCall.call(self, settings, d).

done(function () { d.resolveWith(self.requestContext || self, jquery.makeArray(arguments)); }); } else { router.navigate('#forbidden'); } }); } else { self.refreshToken().

then(function () { self.makeServiceCall.call(self, settings, d).

done(function () { d.resolveWith(self.requestContext || self, jquery.makeArray(arguments)); }); }); } } else { d.rejectWith(self.requestContext || self, jquery.makeArray(arguments)); } }); return d; },

Кроме того, мы решили множество интересных задач, которые не приводим здесь, поскольку их описание заняло бы целую книгу.



День Х – 1 месяц

Закончив работу в тестовой среде, мы доработали инструкцию, собрали огромный пакет для обновления и провели тестовую миграцию на предрелизный сервер с реальными данными.

Три дня мы боролись с возникавшими мелочами (а без них тестовая среда не идеальна), и зарядили тестирование с самого начала, после чего был назначен День Х.



День Х

День Х был назначен на субботу, встречаемся в офисе в 9-00. В офисе клиента есть свой администратор, который собственно и осуществляет развертывание.

Всегда страшно, когда по твоей инструкции разворачиваешь не себя, а кого-то другого, поэтому целый день мы следили за каждым его шагом через общий экран в Lync. В 15-00 миграция завершена.

Всё проверяем, находим то, что не снялось, и дорабатываем напильником.

В 18-00 все остальное заработало, мы ушли довольные.



Первый рабочий день после дня X

Наступает понедельник, первый рабочий день.

Для нас начинается АД.

Из главного получается, что: а.

Токен выходит из строя гораздо чаще, чем мы думали; б.

Портал тормозит больше обычного; в.

Пользователей постоянно отбрасывает обратно на страницу входа, что очень и очень затрудняет работу; д. Проблема не только с ajax-запросами.

Если пользователь полчаса заполняет стандартную форму нового элемента списка, то с вероятностью 90% он потеряет свои изменения при сохранении.

Получаем доступ к рабочему серверу, анализируем логи и пытаемся понять, что происходит: Первый «сюрприз».

У нас есть собственный загрузчик файлов с превью, который сохраняет временные файлы на диске (для случаев, когда объект и файлы созданы в одном виде и в данный момент некуда прикрепить файлы, а их нужно показать в превью) .

Таким образом, загрузчик сохраняет временные файлы в подкаталоге приложения, например C:\inetpub\Sharepoint\Files, создавая там свои собственные подкаталоги, а затем удаляя их.

Эти удаления приводили к периодической переработке пула приложений.

А поскольку кэш токенов входа в систему Sharepoint просто живет в памяти, вы можете попрощаться со всеми сеансами.

Этот Uploader, надо признать, уже год с нами, и скорее всего он и раньше перегружал пул, но до этого никто толком не замечал; при аутентификации Windows это не приводило к повторному запросу учетных данных :).

В результате целевая папка загрузчика была быстро изменена и вакханалии стало меньше.

Второй «сюрприз».

Иногда процесс начинал поглощать всю память, вызывая перегрузку пула.

Выяснить, что именно загружает портал, когда над ним работают сотни людей, — задача непростая и быстрая.

Анализируем запросы к базе, логи, находим те самые критические места, которые у всех периодически глючат, понимаем, как их сделать по-другому, переписываем, раскатываем.

Дышать становится легче.

Опять же, проблема этой неоптимальности была всегда, но на периодические тормоза раньше не обращали внимания.

Мало ли что тормозит, а при отсутствии прямого доступа к производственному серверу оперативная диагностика значительно усложняется, поэтому и проигнорировали эту проблему.

Портал работает быстрее, он больше не вылетает каждые 5 минут, но перелогин по-прежнему происходит каждые полчаса-час.

И здесь мы открываем основные вещи, которые должны были обнаружить в самом начале — управление временем жизни Logon Token для тех, кому интересны подробности.

msdn.microsoft.com/ru-ru/library/office/hh147183 (v=office.14).

aspx (мы пропустили эту тему, поскольку она особо не поднималась при тестировании.

Проверка отдельного тест-кейса легко занимает 5 минут, и от случая к случаю тестировщик меняет пользователей, а сессия обновляется Длительная работа с 9 до 6. Мы не эмулировали сессию 1 день и вроде все было хорошо, но.

Еще один сюрприз.

Пользователей продолжают выгонять.

Уже не так регулярно, но случаи есть, и массовые.

Судя по логам, пул в это время не перегружен.

В чем дело? Читаем, разбираемся как это все работает, находим полезную статью про кэши blogs.msdn.com/b/besidethepoint/archive/2013/03/27/appfabric-caching-and-sharepoint-1.aspx Мы понимаем, что по умолчанию размер кеша только на 250 токенов, а когда кеш заполнится, всем привет :) Увеличиваем размер кеша - наступает эйфория.

Поток негатива и гневных писем утихает.

Что дальше

В принципе, на этом можно было бы и закончить, но любопытство берет верх.

Есть еще один момент, когда сессии гаснут. Специфика бизнеса и темпы развития таковы, что редко день проходит без «исправления».

В момент хотфикса приходится перезагружать пул приложений, и тогда все побежали перелогиниваться.

Мы начали смотреть, читать, изучать, что можно сделать в этой ситуации.

Как обычно, не было литературы, подробно описывающей механизмы работы модулей SharePoint, поэтому мы пошли на тяжелую артиллерию — анализировали модули, подключаемые к SharePoint, разбирали сборки, смотрели, как все это работает изнутри.

День исследования и поиска злодея:

<add type="Microsoft.SharePoint.IdentityModel.SPTokenCache, Microsoft.SharePoint.IdentityModel, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />

И в нем SPSecurityTokenCache приватная KeyValuePair [] m_StrongCache; — так внутри реализован тот самый StrongCache. Ради интереса мы решили попробовать написать собственный TokenCache, но попытка сделать это за 5 минут не увенчалась успехом.

Покопавшись поглубже во всем конвейере SP, мы обнаружили, что связность компонентов там достаточно высока.

В итоге мы все-таки написали собственную версию TokenCache для целей тестирования, хотя кое-где все же пришлось использовать Reflection.

Заключение

Какие выводы можно сделать, оглядываясь назад? Подобные изменения подобны замене кирпича в основании пирамиды, поддерживающего всю конструкцию.

Конечно, можно сказать, что этого делать не нужно, что систему нужно дорабатывать, а не перестраивать.

Однако в реальной жизни обязательно наступит момент, когда станут необходимы фундаментальные изменения.

И тогда, на наш взгляд, вам необходимо: а.

Тщательно изучить внутренние механизмы того, что подвергнется изменениям, сколько бы поставщики ни говорили о «хорошо документированном черном ящике»; б.

Максимально смоделируйте работу системы, включая сценарии длительного использования, высокой нагрузки и т. д.; в.

Постарайтесь предугадать последствия.

Проработать сценарии отката, если изменения придется удалить; д. Согласуйте с клиентом, что возможны сбои; е.

И готовься.

Теги: #ADFS #sharepoint #единый вход #sharepoint

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.