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

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось1
До обраногоВ обраному0
LinkedIn



6 коментарів

Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.
«устаревшая» в данном случае означает систему, которая поддерживает только HTTP 1.0. А это значит — никакой поддержки HTTP keep-alive

те, що це HTTP 1.0, не обов’язков означає, що сервер (що за сервер, до речі ?) не підтримує Keep-Alive header-и.

Так, згоден — технічно ніщо не заважає додати підтримку Keep-Alive до HTTP 1.0. Сервер — IIS 7, але справа зовсім не у ньому. Річ у тім, що Веб-служби застарілої системи реалізовано у формі ISAPI розширення. А ISAPI розширенням дозволено самим керувати станом HTTP з’єднання. Тобто, вони через відповідні API можуть або залишити з’єднання відкритим (і тоді IIS надішле клієнту у відповідь заголовок Connection: Keep-Alive), або попросити IIS примусово закрити з’єднання.

Так от, застаріла система із статті, на жаль, реалізована таким чином, що завжди закриває з’єднання, навіть якщо з клієнта прийшов заголовок Connection: Keep-Alive. Напевне, автори вирішили, що якщо вони не обіцяли підтримки HTTP 1.1, то витрачати час та гроші на підтримку keep-alive — марна справа :)

Напевне, автори вирішили, що якщо вони не обіцяли підтримки HTTP 1.1, то витрачати час та гроші на підтримку keep-alive — марна справа :)

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

Затраты на keep-alive транспорт как правило всегда решаються из коробки веб сервером

Как правило — да. Но, в случае ISAPI расширений для IIS, требуются хотя бы минимальные телодвижения и со стороны самого расширения. Впрочем, глядя на то, как в принципе в устаревшей версии этой системы прикостылены Веб-службы, я ни разу не удивлён, что разработчикам было целиком и полностью наплевать на keep-alive.

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

Печаль, да. Но, как говорится, ничего личного, это бизнес. Есть работающая business-critical система. Примерная оценка полной стоимости её апгрейда показала, что бизнес не готов столько инвестировать, и костыли обойдутся дешевле. Тем более, костыль делается один раз, и для всех остальных эта устаревшая система будет видна через REST / OData адаптер (в частности, OData позволит легко подключать эту систему как источник данных к внутренним SharePoint решениям).

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

Писать/саппортить бизнесс-систему, которая предполагает работу на уровне ISAPI в IIS в наше время?
Не видел такого ранее, что бы опции миграций постепенных с выпуском MVP, компонентной поэтапной замены и т.д. полностью не работали, тем более если бизнесс готов строить поверх этого legacy современные АПИ и платить за это.

Писать/саппортить бизнесс-систему, которая предполагает работу на уровне ISAPI в IIS в наше время?

В виде ISAPI реализованы Веб-службы устаревшей системы. Поскольку это сторонний продукт с закрытыми исходниками, то поделать с этим мы, увы, ничего не можем. С самой этой системы бизнес в обозримом будущем не уйдёт, т.к. под нее разработана туева хуча расширений под специфику наших бизнес-процессов.

Современный API поверх этого legacy написан на .NET / OWIN / Katana, там с модульностью всё в порядке.

Підписатись на коментарі