Оптимизация производительности делегации Kerberos при обращении к устаревшей системе
Любой, кто хоть раз имел дело с делегированием Kerberos, знает, как непросто все правильно настроить. Тонкости и сложности выдачи прав и грамотное конфигурирование SPN кого угодно заведут в тупик. Но иногда и этим дело не ограничивается — особенно, когда в конечном итоге вам нужно обращаться к устаревшей ERP системе. Об этом я и хочу сегодня рассказать.
Начнем с известного факта, что аутентификация Kerberos основана на запросах. То есть каждый запрос к удаленной системе должен содержать действующий мандат Kerberos в HTTP-заголовке Authorization. Вроде бы это не такая уж и большая проблема: мы живем в 2017 году, и пару лишних килобайт данных (да, мандаты Kerberos не столь маленькие, как кажется, и могут достигать 16 Кб) большой роли не сыграют. На самом деле проблема не столько в размере передаваемых данных, сколько в необходимости проверять входящий мандат Kerberos для каждого запроса — что, конечно же, не очень хорошо масштабируется. На всякий случай имейте в виду, что размер мандата и сам по себе может приводить к проблемам, поскольку http.sys может не принимать HTTP-запросы больше 16 килобайт.
Поэтому, чтобы избежать снижения производительности, специалисты из Майкрософт добавили в IIS специальную настройку, которая называется authPersistNonNTLM и позволяет проводить аутентификацию Kerberos только один раз для каждого TCP-соединения. Любопытного читателя может заинтересовать прекрасный пост в блоге, в котором описываются все детали этой настройки. Кто-то скажет: эй, так давайте же просто включим эту настройку и пойдём пить пиво? Не так быстро, дорогой друг, не так быстро — у нас здесь устаревшая система! :) Если точнее, «устаревшая» в данном случае означает систему, которая поддерживает только HTTP 1.0. А это значит — никакой поддержки HTTP keep-alive, что, в свою очередь, приводит к невозможности использовать одно TCP-соединение для нескольких HTTP-запросов. Следовательно, увы и ах — настройка authPersistNonNTLM становится совершенно бесполезной.
Возникает вопрос: а можно ли все-таки что-то оптимизировать при таких печальных обстоятельствах? И, как оказалось, ответ положительный: мы можем добиться повышения производительности, используя метод под названием «пре-аутентификация». Основная идея этого метода крайне проста: вместо дополнительного обращения к серверу после получения ответа 401 Unauthorized, мы заранее добавляем действительный мандат Kerberos к каждому исходящему запросу, избавляясь, таким образом, от лишних обращений к серверу по сети. И все было бы просто, если бы WCF поддерживал пре-аутентификацию по умолчанию. К сожалению, по какой-то странной причине, такой поддержки в WCF «из коробки» нет, и мы остаемся один на один с задачей получения действующего мандата Kerberos и добавления его в качестве правильно сформированного заголовка Authorize. Желательно, не обращаясь к низкоуровневым SSPI API — потому что, вы же знаете, там обитают драконы :)
После множества проб и ошибок мы пришли к следующему:
public dynamic CallSoapMethod<TService>(Func<TService, Task<dynamic>> func, string endPointName) { dynamic service = Activator.CreateInstance(typeof(TService), endPointName); using (new OperationContextScope(service.InnerChannel)) { HttpRequestMessageProperty requestMessage = new HttpRequestMessageProperty(); var contextChannel = (IContextChannel)service.InnerChannel; var remoteAddressUri = contextChannel.RemoteAddress.Uri; ICredentials credentials = CredentialCache.DefaultCredentials; NetworkCredential credential = credentials.GetCredential(remoteAddressUri, "Kerberos"); KerberosSecurityTokenProvider tokenProvider = new KerberosSecurityTokenProvider( "HTTP/your-service-SPN", System.Security.Principal.TokenImpersonationLevel.Impersonation, credential); KerberosRequestorSecurityToken securityToken = tokenProvider.GetToken( TimeSpan.FromMinutes(1)) as KerberosRequestorSecurityToken; var securityTokenRequest = securityToken.GetRequest(); string serviceToken = Convert.ToBase64String(securityTokenRequest); requestMessage.Headers["Authorization"] = "Negotiate " + serviceToken; OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = requestMessage; return func.Invoke(service); } }
ВАЖНО: В web.config веб-сайта / веб-сервиса, который обращается к устаревшей системе, должно быть следующее:
<system.web> <authentication mode="Windows" /> <identity impersonate="true" /> </system.web>
иначе делегация идентификатора Kerberos работать не будет.
Что же можно вынести из нашего опыта — ведь о том, что работа с устаревшими системами грозит кучей проблем, вы наверняка знали и до нас? :) Думаем, итог можно подвести такой — именно в ситуациях, когда не подходят стандартные решения, и приходится изобретать и выкручиваться, проявляется истинный профессионализм IT-специалиста, который больше всего ценится заказчиками.