Веб-сервисы, защищенные посредством промежуточного программного обеспечения, ориентированного на обработку сообщений - Обновление серверной службы для использования WSE для защищенной связи

ОГЛАВЛЕНИЕ

Обновление серверной службы для использования WSE для защищенной связи

Сервис является всего лишь выполняемым файлом, отбирающим сообщения из очереди, он существует вне контейнера, предоставляемого веб-сервисами ASP.NET, и поэтому нужно использовать средства неформатированных конвейеров WSE для запуска конвейера входного фильтра при проверке подлинности подписей и расшифровке содержимого с целью добраться до полезной нагрузки. С другой стороны, так как поддерживается двусторонняя защита, также нужно запускать конвейер выходного фильтра, чтобы сервисы шифрования и подписывания активизировались перед возвратом ответа Soap (по сути, вся работа, бесплатно выполняемая в традиционно размещенном на HTTP веб-сервисе ASP.NET).

Подготовка SecurityProvider

Нужно установить значения конфигурации, рассмотренные ранее в разделе "Архитектура SecurityProvider". Они направляют WSE в нужное место при запрашивании пароля пользователя и симметричного ключа для расшифровки.

Вызов конвейера входящего сообщения для запросов

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

// Получаем содержимое сообщения прямо из BodyStream
string strResp = string.Empty;
byte[] bufIn = new Byte[objRequestMsg.BodyStream.Length];
objRequestMsg.BodyStream.Position = 0;
objRequestMsg.BodyStream.Read(bufIn, 0, (int) objRequestMsg.BodyStream.Length);
string strCorrelationId = objRequestMsg.Id;

// Обрабатываем через WSE – это может быть сквозной операцией,
// если WSE не использовался для создания потока
               
// Нужно выяснить, основан поток на DIME или нет
// Для этого нужно выяснить, удается ли прочитать поток
// как поток DIME – оболочка создается в любом случае
SoapEnvelope env = InputStreamToEnvelope(objRequestMsg.BodyStream);

// Закрываем поток
objRequestMsg.BodyStream.Close();

// Создаем конвейер – с набором стандартных фильтров для ввода и вывода
// (т.е. стандартное поведение не переопределяется)
Pipeline objWSEPipe = new Pipeline();
               
// Запускаем стандартный входной конвейер
objWSEPipe.ProcessInputMessage(env);

// Извлекаем полученную оболочку
bufIn = new UTF8Encoding().GetBytes(env.OuterXml);

// Получаем очередь ответов – нужно брать стандартную, но
// надеяться, что клиент передал очередь для обратного вызова
string strResponseQueue = vstrQueueName + "_resp";
if (objResponseMsg.ResponseQueue != null)
    strResponseQueue = objRequestMsg.ResponseQueue.QueueName;

Выделенный полужирным шрифтом текст показывает формирование объекта SoapEnvelope из потока входящего сообщения. После получения данной оболочки (подробней о способе ее получения далее) создается конвейер WSE и запускаются стандартные входные фильтры на оболочке. Содержимое входного конвейера не имеет значения (так как маршрутизация не используется, можно программно удалить фильтры маршрутизации и направления из входного конвейера, что немного повышает скорость запуска конвейера).

Вызов ProcessInputMessage() является неделимой операцией, приводящей к записи чистой текстовой версии сообщения Soap в переменную env. В качестве побочного эффекта запуска свойство Context в объекте оболочки заполняется расшифровкой входящего сообщения Soap.

Поддержка SecurityProvider автоматически подключается к процессу в ходе запуска конвейера, если заголовки шифрования или подписи обнаруживаются во входящем сообщении Soap.

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

Вызов конвейера исходящего сообщения для ответов

После того как сообщение запроса было согласовано и сообщение ответа сгенерировано, нужно решить, защищать ли сообщение ответа. Синтезированное свойство env.Context показывает, участвовал ли WSE в формировании сообщения запроса. Это свойство – экземпляр класса SoapContext, рассмотренного выше:

// Выясняем, участвовал ли WSE...
if (vobjSoapContext != null)
{
    if (vobjSoapContext.Attachments.Count > 0 ||
         vobjSoapContext.Security.Elements.Count > 0 ||
        vobjSoapContext.Security.Tokens.Count > 0)
          strWSEIsInvolved = " {WSE}";
}

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

// Нужно ли применять WSE к этому сообщению ответа перед его передачей
// протоколу передачи? Это нужно делать, если имеется сообщение ответа
// для обработки, и запрос был обработан WSE
if (strResp.Length
> 0  && strWSEIsInvolved.Length
    >  0)
{
    // Да. Обрабатываем с помощью WSE

    // Создаем оболочку
    SoapEnvelope env = new SoapEnvelope();

    // Вставляем базовую информацию Soap в оболочку
    env.LoadXml(strResp);

    // Пополняем контекст запроса средствами WSE
    // Они включают в себя:
    //
    //    * Время жизни 60с
    //    * Данные имени пользователя и пароля
    //    * Шифрование и подписывание
    // Добавляем экземпляр зашифрованных данных в ответ SOAP.
    WSCommon.SetupSOAPRequestContext(env.Context);

    // Создаем конвейер – со стандартным набором фильтров для ввода и вывода
    // (т.е. стандартное поведение не переопределяется)
    Pipeline objWSEPipe = new Pipeline();

    // Запускаем стандартный выходной конвейер
    objWSEPipe.ProcessOutputMessage(env);

    // Извлекаем полученную оболочку
    strResp = env.OuterXml;
}

Свойство env.Context заполняется перед запуском выходного конвейера. Используется копия того же самого алгоритма SetupSOAPRequestContext(), описанного выше в разделе "Изменения в вызывающем коде", используемого стороной клиента.

Обновление серверной службы для использования WSE для двоичных вложений

Можно использовать WSE для отправки и приема двоичных вложений, которые не нужно шифровать в виде base64Binary внутри оболочки Soap. Во время чтения входящих сообщений Soap нужно искать признаки приема одного или более сообщений DIME.

DIME – спецификация, описывающая возможность переносить двоичное содержимое вне оболочки Soap и обращаться к ней изнутри. Это имеет плюсы и минусы – двоичные данные не шифруются и поэтому имеют меньший размер, чем эквивалентное представление веб-сервисами параметра API, например, типа byte[]. Однако перенос вне оболочки Soap порождает две главные проблемы: данные нельзя выразить в виде параметра или описать в WSDL, и хуже того, невозможно применять такую же модель обработки Soap (включая средства подписания и шифрования) к содержимому DIME. Пока придется смириться с такими ограничениями, но необходимо  следить за этой областью.

[Примечание: Сейчас выполняется работа по объединению метода работы DIME с требованием наличия единой модели обработки Soap. Ожидается, что двоичные вложения будут возвращаться назад в оболочке Soap, где они будут подчиняться тем же правилам, что и другие элементы. Ожидается, что они сохранят свой эффективный формат без шифрования Base64 несмотря на это изменение.]

Обработка DIME реализуется полностью независимо от обработки фильтра WSE (потому что модель обработки DIME полностью отличается от стандартной модели Soap, предназначенной для выполнения действий над содержимым оболочки Soap), поэтому требуются дополнительные усилия для правильного заполнения свойства набора SoapContext.Attachments. Это осуществляет метод InputStreamToEnvelope(), обойденный молчанием в разделе "Вызов конвейера входящего сообщения для запросов":

// Примечание – некоторая часть кода пропущена для уменьшения размера
SoapEnvelope env = new SoapEnvelope();

try
{
   // Создаем считыватель для сообщения DIME
   DimeReader dr = new DimeReader(vstInputMessage);

   // Пытаемся прочитать запись, содержащую сообщение SOAP. Если это не удается с
   // ошибкой версии DIME, скорее всего, поток не является DIME
   DimeRecord rec = dr.ReadRecord();

   // Читаем сообщение Soap из записи DIME
   env.Load(rec.BodyStream);

   // Добавляем все вложения, обнаруженные в этом потоке, в контекст Soap
   while (rec != null)
   {
      // Читаем следующую запись
      rec = dr.ReadRecord();

      // Проверяем, прочиталась ли запись
      if (rec != null)
      {
         // Извлекаем из записи двоичные данные, представляющие ее
         BinaryReader stream = new BinaryReader(rec.BodyStream);
         …
         // Сохраняем двоичные данные в новый поток
         …
         stNew.Write(bytAttachmentItem, 0, bytAttachmentItem.Length);

         // ... этот поток прикрепляется к контексту Soap
         DimeAttachment objBin = new DimeAttachment(
                rec.Id, rec.Type, rec.TypeFormat, stNew);
         env.Context.Attachments.Add(objBin);
      }
   }
}
catch(Exception)
{
   // Если здесь выдается исключение, поток не является DIME

   // Теперь загружаем весь поток в массив байтов
   byte[] bufIn = new Byte[vstInputMessage.Length];
   vstInputMessage.Read(bufIn, 0, (int)vstInputMessage.Length);

   // ...массив байтов заключаем в оболочку Soap
   env.LoadXml(new UTF8Encoding().GetString(bufIn));
}

return env;

После завершения вышеуказанной загрузки серверный код реагирует на содержимое свойства набора SoapContext.Attachments следующим образом:

// Ищем вложение DIME - должно быть только одно
DimeAttachmentCollection colAttachments = vobjSoapContext.Attachments;
   
// Найдены ли какие-либо вложения?
if (colAttachments.Count >  0)
{
    // Получаем только первое вложение – это всего лишь демо!
    // Извлекаем из вложения двоичные данные, представляющие его
    BinaryReader stream = new BinaryReader(colAttachments[0].Stream);
    byte[] bytAttachmentItem = new byte[stream.BaseStream.Length];
    stream.Read(bytAttachmentItem, 0, bytAttachmentItem.Length);
    stream.BaseStream.Close();
    stream.Close();

    // Записываем двоичные данные в файл – знаем, что это gif, но можем
    // проверить тип, как описано в объекте DimeAttachment.
    FileStream fs = new FileStream(vstrLocOfImages + "SendDIMEImage" +
                        vstrScheme +
                        strWSEIsInvolved + ".gif",
                        FileMode.Create);
    fs.Write(bytAttachmentItem, 0, bytAttachmentItem.Length);
    fs.Close();
}

Сводка опций клиента с WSE или без WSE

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

Сценарий

Требования к генерируемому посреднику

Требования к вызывающему оператору посредника

Без WSE

  • Получен из MySoapClientProtocol
  • Реализует переопределенный GetWebRequest()и GetWebResponse()
  • При желании устанавливает свойство ResponseUrl (по умолчанию Url + "_resp")

С WSE

  • Получен из MyWSESoapClientProtocol
  • Implement overridden GetWebRequest() only
  • При желании устанавливает свойство ResponseUrl (по умолчанию Url + "_resp")
  • Устанавливает свойство SoapContext с помощью SetupSOAPRequestContext()
  • Устанавливает конфигурационный файл, добавляя в него данные о SecurityProvider

Параллельность в формировании очередей

Описанное ниже пробное приложение было протестировано в режиме "несколько клиентов – один сервер". В этом режиме клиенты помещают сообщения в очередь "одиночного запроса" одновременно и ожидают ответа на разных "очередях ответа" одновременно. Цель – выделить все проблемы в идентификации очередей и сообщений и доступе при наличии нескольких пользователей.

При первом тестировании протокол передачи MQ работал безупречно при наличии до 30 клиентов, а протокол передачи MSMQ регулярно выбрасывал исключения вида:

"Сообщение, на которое в настоящий момент указывает указатель, было удалено из очереди".

Причина в том, что API ReceiveByCorrelationID()не транзакционный, и поэтому в момент, когда он берет поток активности для идентификации сообщения по идентификатору соответствия, указатель, помечающий сообщение, может стать недействительным. Наблюдение показало, что при наличии 20 процессов, каждый из которых ожидает 100 связанных сообщений на одной и той же очереди MSMQ, вышеприведенное исключение выбрасывается в среднем 2,7 раз за прогон (за 2000 считываний сообщений) – процент ошибок 0,135%.

Если такая ошибка происходит, прием нужно повторять. Поэтому в коде присутствует цикл.

Упаковка каркаса

Классы, описанные в статье, образующие каркас, упакованы в сборку под именем WSQTransports.dll:

Рисунок 16. Упаковка WSQTransports

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