Управление состоянием

Глава 6 из книги “Microsoft ASP.NET 2.0 с примерами на C# 2005 для профессионалов”

Авторы: Мэтью Мак-Дональд
Марио Шпушта

Источник: Microsoft ASP.NET 2.0 с примерами на C# 2005 для профессионалов
Материал предоставил: Издательство ''Вильямс''
Опубликовано: 06.09.2006
Версия текста: 1.0
Изменения, касающиеся управления состояниями, в .NET 2.0.
Управление состоянием в ASP.NET
Состояние просмотра
Пример использования состояния просмотра
Сохранение объектов в состоянии просмотра
Сохранение переменных экземпляра
Оценивание преимуществ использования состояния просмотра
Настройка состояния просмотра в элементе управления типа списка (List)
Защита состояния просмотра/обеспечение безопасности состояния просмотра
Передача информации
Строка запроса
Использование строки запроса
URL-Кодировка URL-адреса
Пересылка данных между страницами
Получение/извлечение специфической информации о странице
Выполнение межстраничной пересылки данных в любом обработчике событий
Межстраничная пересылка и проверка данных
Специальные cookie-наборы
Состояние сеанса
Архитектура сеанса
Использование состояния сеанса
Конфигурирование состояния сеанса
Обеспечение безопасности состояния сеанса
Состояние приложения
Статические переменные приложения
Заключение

Ни одна среда для разработки Web-приложений, какой бы усовершенствованной она не была, не может изменить тот факт, что HTTP является протоколом, который не сохраняет никакой информации о состоянии. После первого Web-запроса, клиент отключается от сервера, и механизм ASP.NET удаляет/очищает объекты страницы. Такая архитектура позволяет Web-приложениям обслуживать одновременно тысячи запросов, не истощая полностью ресурсы памяти сервера. Недостатком является то, что код должен использовать другие технологии для хранения информации между Web-запросами и ее извлечения при необходимости.

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

Изменения, касающиеся управления состояниями, в .NET 2.0.

Основные принципы управления состоянием остались прежними в ASP.NET 2.0. Это означает, что интерфейс программирования для состояния сеанса, состояния приложения, состояния просмотра и строки запроса совсем не изменился. Однако, для состояния сеанса теперь доступно больше опций/вариантов конфигурирования, а также появился новый способ для передачи информации между страницами.

Ниже приводится краткое описание всех изменений:

Из этих четырех изменений в этой главе будут рассмотрены только первые два. Профили будут рассматриваться в главе 24, потому что они могут оказаться полезными при настройке системы аутентификации Windows или системы аутентификации с помощью форм. Рассмотрение специальных поставщиков состояний сеанса выходит за рамки контекста данной книги, однако с поставщиками сеансов сторонних производителей, которые позволяют использовать сеансы с другими реляционными базами данных, у читателя будет возможность познакомиться.

Управление состоянием в ASP.NET

ASP.NET включает целый ряд опций для управления состояниями. Прежде всего, ASP.NET реализует те же коллекции состояний Session и Application, что и традиционная технология ASP (с несколькими улучшениями), а также совершенно новую модель состояния просмотра. ASP.NET даже включает систему кэширования, которая позволяет сохранять информацию, не жертвуя степенью масштабируемости сервера. Каждый опция управления состоянием имеет свой срок жизни, контекст, непроизводительные издержки и уровень поддержки.

В табл. 6.1, 6.2, 6.3 приводится краткий сравнительный анализ всех доступных для управления состоянием опций.

Состояние просмотраСтрока запросаСпециальные cookie-наборы
Допустимые типы данныхВсе поддающиеся сериализации .NET-типы данных.Ограниченное количество строковых данных.Строковые данные.
Место храненияСкрытое поле на текущей Web-странице.Строка URL-адреса в браузере.Компьютер клиента (в памяти или небольшом текстовом файле, в зависимости от срока жизни данного cookie-набора).
Срок жизниСохраняется постоянно для отправки данных на одну страницу.Утрачивается, когда пользователь вводит новый URL-адрес или закрывает окно браузера. Однако, может быть сохранена в ссылках.Срок жизни cookie-наборов определяется программистом. Они могут использоваться на нескольких страницах и могут сохраняться между посещениями.
КонтекстОграничивается/-но текущей страницей.Ограничивается целевой страницейВсе ASP.NET-приложение целиком.
БезопасностьПо умолчанию является незащищенным, хотя можно воспользоваться директивами Page и принудительно применить шифрование и хеширование.Доступна для просмотра и запросто может быть изменена пользователем.Никак не защищены и могут изменяться пользователем.
Сложности, влияющие на производительностьХранение большого количества информации замедлит процесс передачи, но никак не отразится на производительности сервера.Никаких, потому что количество данных очевидно.Никаких, потому что количество данных очевидно.
Обычно применяется для:Для настройки параметров конкретной страницы.Для отправки идентификационного номера продукта со страницы каталога на страницу подробностей.Для определения персонализированных предпочтений на Web-сайте
Таблица 6.1. Сравнение опций для управления состоянием (часть I)
Состояние сеанса (Session)Состояние приложения (Application)
Допустимые типы данныхВсе поддающиеся сериализации .NET-типы данных. Не поддающиеся сериализации типы поддерживаются, если используется внутрипроцессная служба состояний. Все .NET-типы данных.
Местонахождение хранилища/Место хранения данныхВ памяти сервераВ памяти сервера.
Время жизниИстекает по прошествии предопределенного периода времени (который как правило длится 20 минут, но может быть изменен глобально или программно).Совпадает со сроком жизни приложения (который обычно длится до перезагрузки сервера).
КонтекстВсе ASP.NET-приложение.Все ASP.NET-приложение. В отличие от большинства других типов методов, данные приложения являются глобальными для всех пользователей.
БезопасностьЯвляется безопасным/защищенным, потому что данные никогда не передаются клиенту. Однако, подвержено атакам типа перехвата сеанса, если не используется SSL. Является очень безопасным, потому что данные никогда не передаются клиенту.
Сложности, влияющие на производительностьСохранение большого количества информации может значительно замедлить работу сервера, особенно в случае, когда к нему подключается одновременно большое количество пользователей, потому что для каждого пользователя будет создаваться отдельная копия данных сеанса.Сохранение большого количества информации может замедлить работу сервера, потому что срок жизни этих данных никогда не будет заканчиваться и они и никогда не будут удаляться.
Обычно применяется:Для хранения элементов в “корзине для покупок”.Для хранения глобальных данных любого типа.
Таблица 6.2. Сравнение опций для управления состояниями (часть II)
ПрофилиКэширование
Допустимые типы данныхВсе поддающиеся сериализации .NET-типы данных. Не поддающиеся сериализации типы поддерживаются, если создается специальный профиль.Все .NET-типы данных.
Местонахождение хранилища/место хранения данныхВ конечной базе данныхВ памяти сервера.
Срок жизниПостоянныйЗависит от применяемой вами политики истечения сроков, но может оканчиваться раньше, если серверу перестает хватать ресурсов памяти.
КонтекстВсе ASP.NET-приложение. К ним также могут доступ и другие приложения.Тот же, что и состояния приложения (глобальный для всех пользователей и всех страниц).
БезопасностьЯвляются довольно безопасными, потому что хотя данные и никогда не передаются клиенту, они хранятся в базе данных, которая может быть “взломана”. Является достаточно безопасным, потому что данные никогда не передаются клиенту.
Сложности, влияющие на производительностьПозволяют легко сохранять большое количество данных, но не исключают возникновение нетривиальных непроизводительных издержек при извлечении и записи данных для каждого запроса.Сохранение большого количества информации может привести к вытеснению другой более полезной, находящейся в кэше информации. Однако, ASP.NET имеет возможность удалять элементы заблаговременно для гарантии оптимальной производительности.
Обычно применяется:Для хранения информации об учетной записи пользователя.Для хранения данных, извлекаемых из базы данных.
Таблица 6.3. Сравнение опций для управления состояниями (часть III)

Очевидно, что в опциях для управления состояниями в ASP.NET нет недостатка! К счастью, большинство из этих систем управления состоянием реализуют/отображают/предоставляют сходный/жий, базирующийся на коллекциях интерфейс программирования. Двумя исключениями являются: строка запроса (которая на самом деле представляет собой способ передачи информации, а не ее обслуживания) и профили.

В этой главе мы рассмотрим все перечисленные в таблицах подходы к управлению состоянием, кроме тех, которые перечислены в табл. 6.3. Кэширование, незаменимая технология для оптимизации доступа к ограниченным ресурсам, будет подробно рассматриваться в главе 11, а профили, представляющие собой высокоуровневую модель для хранения специфической информации о пользователе, которая работает вместе с системой аутентификации ASP.NET, - в главе 24. Однако, прежде чем приступать к рассмотрению любого из этих вопрос, читатель должен будет внимательно ознакомиться с основными концепциями управления состоянием.

Более того, у разработчика всегда есть возможность написать свой собственный специальный код управления состоянием и использовать для хранения информации ресурсы на стороне сервера, а таковыми чаще всего являются одна или более таблиц в какой-нибудь базе данных. Недостатком такого подхода является то, что использование ресурсов на стороне сервера как правило приводит к снижению производительности и может крайне негативно сказаться на масштабируемости. Например, установка соединения с базой данных или считывание информации из файла занимает определенное время. В большинстве случаев эти процедуры можно ускорить, усовершенствовав имеющуюся систему управления состояниями путем добавления кэширования. Опции для использования и усовершенствования системы получения доступа к базе данных будут рассматриваться в части II этой книги.

Состояние просмотра

Состояние просмотра – это вариант, который должен рассматриваться самым первым при необходимости хранения информации в пределах одной единственной страницы. Web-элементы управления ASP.NET используют состояние просмотра по умолчанию. Оно позволяет им сохранять их свойства между отправками данных (postback). С помощью свойства ViewState вы можете добавить в коллекцию состояния просмотра свои собственные данные. В частности, вы можете здесь хранить простые типы данных и свои собственные специальные объекты.

Как и в основе большинства опций для управления состоянием в ASP.NET, в основе состояния просмотра лежит коллекция типа словаря, где каждый элемент индексируется с помощью уникального строкового имени. Например, взгляните на следующий фрагмент кода:

ViewState["Counter"] = 1;

Этот код помещает в коллекцию ViewState значение 1 (или, скорее, целое число, которое содержит значение 1) и присваивает ему описательное имя Counter. Если в коллекции ViewState на текущий момент нет элемента с именем “Counter”, в нее будет автоматически добавлен такой новый элемент. Если элемент с таким именем уже существует, он будет заменен.

При извлечении значения следует использовать имя ключа. Помимо этого, также следует привести извлекаемое значение к соответствующему типу данных. Этот дополнительный шаг является обязательным, потому что коллекция ViewState сохраняет/хранит все элементы в виде объектов обобщенного типа, что позволяет ей иметь дело со многими различными типами данных.

Ниже показан код, который извлекает значение Counter и преобразовывает его в целое число:

int counter;
if (ViewState["Counter"] != null)
{
   counter = (int)ViewState["Counter"];
}

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

ПРИМЕЧАНИЕ

Многие из предоставляемых ASP.NET коллекций используют синтаксис такого же словарного типа, среди них: коллекции, применяемые для состояния сеанса и приложения, а также коллекции, применяемые для кэширования и cookie-наборов. Некоторые из этих коллекций читатель сможет увидеть в этой главе.

Пример использования состояния просмотра

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

Вот как выглядит весь код:

public partial class ViewStateTest : System.Web.UI.Page
{
  protected void cmdSave_Click(object sender, System.EventArgs e)
  {
    // Сохраняем текущий текст.
    SaveAllText(Table1.Controls, true);
  }

  private void SaveAllText(ControlCollection controls, bool saveNested)
  {
    foreach (Control control in controls)
    {
      if (control is TextBox)
      {
        // Сохраняем текст, используя уникальный идентификатор (ID) 
        // элемента управления.
        ViewState[control.ID] = ((TextBox)control).Text;
      }

      if ((control.Controls != null) && saveNested)
      {
        SaveAllText(control.Controls, true);
      }
    }
  }

  protected void cmdRestore_Click(object sender, System.EventArgs e)
  {
    // Извлекаем последний сохраненный текст.
    RestoreAllText(Table1.Controls, true);
  }

  private void RestoreAllText(ControlCollection controls, bool saveNested)
  {
    foreach (Control control in controls)
    {
      if (control is TextBox)
      {
        if (ViewState[control.ID] != null)
          ((TextBox)control).Text = (string)ViewState[control.ID];
      }
      if ((control.Controls != null) && saveNested)
      {
          RestoreAllText(control.Controls, true);
      }
    }
  }
}

На рис. 6.1. показана эта страница в действии.


Рис. 6.1. Сохранение и восстановление текста с помощью состояния просмотра

Сохранение объектов в состоянии просмотра

В состоянии просмотра можно сохранять свои собственные объекты, причем так же легко как числовые и строковые типы. Однако, чтобы элемент был сохранен в состоянии просмотра, ASP.NET должен быть способен преобразовать его в поток байтов для того, чтобы он мог быть добавлен в скрытое поле для ввода данных на странице. Этот процесс называется сериализацией. Если объекты не поддаются сериализации (а по умолчанию это именно так), при попытке поместить их в состояние просмотра, появится сообщение об ошибке.

Чтобы сделать объекты пригодными для сериализации, следует просто добавить перед объявлением соответствующего класса атрибут Serializable. Например, ниже показан чрезвычайно простой класс Customer:

[Serializable]
public class Customer
{
   public string FirstName;
   public string LastName;
   
   public Customer(string firstName, string lastName)
   {
      FirstName = firstName;
      LastName = lastName;
   }
}

Поскольку класс Customer помечен как пригодный для сериализации/поддающийся сериализации (с помощью атрибута Serializable), он может быть сохранен в состоянии просмотра:

// Сохраняем заказчика в состоянии просмотра.
Customer cust = new Customer("Marsala", "Simons");
ViewState["CurrentCustomer"] = cust;

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

// Извлекаем заказчика из состояния просмотра.
Customer cust;
cust = (Customer)ViewState["CurrentCustomer"];

Чтобы класс поддавался сериализации, он должен отвечать следующим требованиям:

Поняв эти принципы, вы также сможете определять, какие .NET-объекты можно помещать в состояние просмотра. Просто отыщите в разделе “Help” (Справка) Web-сайта MSDN подраздел, посвященный классам, а затем отыщите в нем интересующий вас класс и изучите прилагаемую к нему документацию. Если объявлению класса предшествует атрибут Serializable, значит, объект может быть помещен в состояние просмотра. Если атрибут Serializable отсутствует, значит, объект не поддается сериализации, и сохранить его в состоянии просмотра не получится. Однако, скорее всего у вас все равно будет возможность использовать какую-нибудь другую опцию для управления состоянием, например внутрипроцессное состояние сеанса, которое будет описываться чуть позже в этой главе, в разделе “Состояние сеанса”.

Ниже показан измененный код страницы из приведенного ранее примера, который теперь использует класс Hashtable. Класс Hashtable - это поддающаяся сериализации коллекция типа словаря, которая хранится в пространстве имен System.Collections. Поскольку этот класс поддается сериализации, он запросто может быть сохранен в состоянии просмотра. Чтобы продемонстрировать это, вся информация элементов управления страницы сохраняется в классе Hashtable, после чего класс Hashtable добавляется в состояние просмотра этой страницы. Когда пользователь щелкает на кнопке Display (Отобразить), класс Hashtable извлекается и вся содержащаяся в нем информация отображается в элементе управления типа надписи (Label).

public partial class ViewStateObjects : System.Web.UI.Page
{
   // Этот класс будет создаваться в начале каждого запроса.
   Hashtable textToSave = new Hashtable();

   protected void cmdSave_Click(object sender, System.EventArgs e)
   {
      // Помещаем текст в класс Hashtable.
      SaveAllText(Table1.Controls, true);

      // Сохраняем всю коллекцию в состоянии просмотра.
      ViewState["ControlText"] = textToSave;
   }

   private void SaveAllText(ControlCollection controls, bool saveNested)
   {
      foreach (Control control in controls)
      {
         if (control is TextBox)
         {
            // Добавляем в коллекцию текст.
            textToSave.Add(control.ID, ((TextBox)control).Text);
         }
         if ((control.Controls != null) && saveNested)
         {
            SaveAllText(control.Controls, true);
         }
      }
   }
   protected void cmdDisplay_Click(object sender, System.EventArgs e)
   {
      if (ViewState["ControlText"] != null)
      {
          // Извлекаем класс Hashtable.
          Hashtable savedText = (Hashtable)ViewState["ControlText"];

         // Отображаем весь текст, просматривая класс Hashtable посредством цикла.
         lblResults.Text = "";
         foreach (DictionaryEntry item in savedText)
         {
            lblResults.Text += (string)item.Key + " = " +
              (string)item.Value + "<br />";
         }
      }
   }
}

На рис. 6.2 показаны результаты тестирования этого кода, полученные после ввода некоторых данных, их сохранения и извлечения.


Рис. 6.2. Извлечение объекта из состояния просмотра

Сохранение переменных экземпляра

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

Существует два основных подхода. Первый – создать процедуру свойства, охватывающую доступ к состоянию просмотра. Например, в предыдущем примере Web-страницы класс Hashtable ControlText можно было бы представить в виде свойства, как показано ниже:

private Hashtable ControlText
{
   get
   {
      if (ViewState["ControlText"] != null)
        return (Hashtable)ViewState["ControlText"];
      else
        return new Hashtable();
   }
   set {ViewState["ControlText"] = value;}
}

В этом случае остальной код страницы мог бы/далее в коде страницы можно было бы запросто использовать свойство ControlText, не волнуясь о том, как оно извлекается.

Другой подход – сделать так, чтобы все переменные экземпляра сохранялись в состоянии просмотра, когда происходит событие Page.PreRender, а извлекались из него, когда происходит событие Page.Load. В этом случае все остальные обработчики событий смогут использовать переменные экземпляра обычным образом.

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

Оценивание преимуществ использования состояния просмотра

Применение состояния просмотра является идеальным вариантом, потому что не предполагает использования никаких ресурсов памяти сервера и появления случайных ограничений (типа таймаута). Итак, что же может заставить вас отдать предпочтение не состоянию просмотра, а какому-нибудь другому типу управления состоянием? Ниже перечислены три возможных причины:

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

<%@ Page Language="c#" Trace="true" ... %>

Отыщите раздел Control Tree. Итоговых сведений о состоянии просмотра, используемом страницей, вы в этом разделе не найдете, но зато в нем, в столбце Viewstate Size Bytes (Размер данных в состоянии просмотра, в байтах), будут содержаться сведения о состоянии просмотра, используемом каждым отдельным элементом управления этой страницы (рис. 6.3). На столбец Render Size Bytes (Размер визуализированных данных, в байтах) можно не обращать внимания; он просто отражает размер визуализированных HTML-данных для каждого элемента управления.


Рис. 6.3. Определение состояния просмотра, используемого на странице

СОВЕТ

Вы также можете проанализировать содержимое текущего состояния просмотра страницы, воспользовавшись предлагаемым ASP.NET средством Development Helper, которое описывалось в главе 2.

Чтобы повысить скорость передачи данных на странице, состояние просмотра, если оно не используется, лучше отключить. Хотя отключить состояние просмотра можно как на уровне приложения, так и на уровне страницы, наиболее правильный вариант – отключить его на уровне элемента управления. Элемент управления не нуждается в состоянии просмотра в трех следующих случаях:

СОВЕТ

Не забывайте о том, что состояние просмотра применяется ко всем значениями, которые изменяются, а не только к тексту, который отображается в элементе управления. Например, если вы динамически изменяете цвета, применяемые в элементе управления типа надписи (Label), вы должны будете использовать состояние сеанса, даже если вы не устанавливаете динамически текст для этого элемента управления. С технической точки зрения, использование состояния просмотра входит в обязанности элемента управления, поэтому возможен такой вариант: создать серверный элемент управления, который не будет сохранять определенные значения, даже когда состояние просмотра включено. Такая технология в определенных сценариях может применяться для оптимизации производительности.

Чтобы отключить состояние просмотра для какого-нибудь одного элемента управления, установите для свойства EnableViewState этого элемента управления значение false. Чтобы отключить состояние просмотра для всей страницы и всех отображаемых на ней элементов управления, установите значение false для свойства EnableViewState этой страницы или используйте в директиве Page атрибут EnableViewState, как показано ниже:

<%@ Page Language="c#" EnableViewState="false" ... %>

Даже если вы отключите состояние просмотра для всей страницы, вы все равно увидите скрытый дескриптор состояния просмотра с небольшим количеством информации. Это связано с тем, что ASP.NET всегда сохраняет как минимум иерархию элементов управления для страницы, даже если состояние просмотра было отключено. Так что удалить этот последний небольшой фрагмент данных никак не получится.

Настройка состояния просмотра в элементе управления типа списка (List)

В некоторых элементах управления, отключение состояния просмотра может привести к исчезновению предоставляемой им функциональной возможности/к отключению выполняемой им функции/к нарушению их функции. Хотя с появлением состояния элемента управления (привилегированный раздел используемого элементом управления состояния просмотра, о котором более подробно будет рассказываться в главе 27) ситуация несколько улучшилась, некоторые проблемы все же остаются. Это особенно касается существующих элементов управления, которые порой никак нельзя обновить так, чтобы они использовали состояние элемента управления, без внесения изменений в их поведение, что может нарушить работу существующих страниц ASP.NET 1.x.

Примером могут служить элементы управления типа списка (такие, как ListBox (Поле со списком) и DropDownList (Выпадающий список)), отслеживающие выбираемое в них значение. Представим, что создается страница с выпадающим списком, который необходимо заполнить сотнями элементов. Если создание этого списка не является дорогостоящей операцией (например, если он извлекается прямо из памяти или кэша), можно отключить состояние просмотра для данного элемента управления и воссоздавать список в начале каждой операции обратной отправки данных (postback). Ниже в качестве примера показан фрагмент кода, который просто заполняет список числами:

protected void Page_Load(object sender, EventArgs e)
{
  for (int i = 0; i < 1000; i++)
  {
    lstBig.Items.Add(i.ToString());
  }
}

Здесь возникает следующая проблема: отключение состояние просмотра приводит к тому, что после каждой отправки данных со страницы, значение, выбранное пользователем в списке, обязательно будет утрачиваться. Это означает, что извлечь какую-либо информацию из свойства SelectedIndex или SelectedItem не получится. Более того, не будет инициироваться событие SelectedIndexChanged .

Есть только один способ решить эту проблему. Хотя информация о выбранном значении утрачивается, само выбранное пользователем значение все-таки сохраняется в коллекции Request.Forms (это коллекция опубликованных значений, которая существует для обратной совместимости со ASP-страницами). А значит мы можем выполнить поиск выбранного пользователем значения с помощью имени элемента управления и воспользоваться следующим кодом, чтобы восстановить соответствующий выбранный индекс:

protected void Page_Load(object sender, EventArgs e)
{
   for (int i = 0; i < 1000; i++)
   {
      lstBig.Items.Add(i.ToString());
   }
   if (Page.IsPostBack)
   {
         lstBig.SelectedItem.Text = Request.Form["lstBig"];
   }
}
ПРИМЕЧАНИЕ

Очевидно, что это может служить примером ситуации, в которой стоит подумать о том, как сделать пользовательский интерфейс более удобным для использования. Например, неплохой вариант – задать пользователю какой-нибудь предварительный вопрос, сужающий количество элементов в списке, или вообще смоделировать весь процесс с помощью элемента управления Wizard (Мастер). Но, в любом случае, при наличии необходимости в создании списка с большим количеством значений, придется подбирать способ оптимизировать процесс использования его состояния просмотра.

Защита состояния просмотра/обеспечение безопасности состояния просмотра

Как уже упоминалось в предыдущих главах, информация состояния просмотра хранится в виде одной строки в кодировке Base64, что выглядит примерно так:

<input type="hidden" name="__VIEWSTATE" value="dDw3NDg2NTI5MDg7Oz4="/>

Поскольку это значение не отформатировано как открытый текст/не выглядит как открытый текст, многие ASP.NET-программисты считают, что их данные в состоянии просмотра являются зашифрованными. Это не так. Любой достаточно сообразительный хакер может обработать такую строку в обратном порядке и просмотреть сохраненные в состоянии просмотра данные в считанные секунды, как показывалось в главе 3.

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

Сделать это можно добавив в директиву Page в .aspx-файле атрибут EnableViewStateMAC, как показано ниже:

<%@ Page EnableViewStateMAC="true" %>

Хеш-код – это надежно зашифрованная контрольная сумма. По сути, ASP.NET вычисляет эту контрольную сумму на основе текущего содержимого состояния просмотра и добавляет ее в скрытую область ввода, когда возвращает страницу. Когда страница отправляется обратно, ASP.NET заново вычисляет эту контрольную сумму и проверяет ее соответствие. Если злонамеренный пользователь изменит данные состояния просмотра, ASP.NET сможет обнаружить внесенные им изменения и отклонит/отменит обратную отправку данных.

Функция, отвечающая за создание хеш-кодов, по умолчанию является включенной, поэтому если она понадобится, никаких дополнительных шагов выполнять не придется. В некоторых случаях, разработчики предпочитают отключать ее, чаще всего для того, чтобы избежать проблем при работе с группой Web-серверов, где каждый сервер имеет/использует свой ключ. (Проблема возникает тогда, когда страница отправляется обратно и обрабатывается уже каким-нибудь новым сервером, который не способен проверить правильность информации состояния просмотра). Отключить функцию хеш-кодов можно воспользовавшись атрибутом enableViewStateMac в элементе <pages> файла web.config или machine.config, как показано ниже:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
  <system.web>
    <pages enableViewStateMac="false" />
    ...
  </system.web>
</configuration>
ПРИМЕЧАНИЕ

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

Даже когда используются хеш-коды, данные состояния сеанса все равно остаются доступными для чтения. Чтобы полностью исключить возможность получения пользователями доступа к любой информации в состоянии просмотра, следует включить для состояния просмотра функцию шифрования. Включить функцию шифрования для отдельной страницы можно установив в директиве Page соответствующее значение для свойства ViewStateEncryptionMode:

<%@Page ViewStateEncryptionMode="Always">

или же установив подходящее значение для такого же атрибута в конфигурационном файле web.config:

<pages viewStateEncryptionMode="Always">

В любом случае это приведет к применению шифрования. Существует всего три допустимых значения: Always (указывает, что шифрование должно применяться всегда), Never (указывает, что шифрование не должно применяться никогда) и Auto (указывает, что шифрование должно применяться только, если элемент управления специально запрашивает его). По умолчанию используется значение Auto, что означает, что какой-нибудь элемент управления должен вызвать метод Page.RegisterRequiresViewStateEncryption(), чтобы запросить шифрование. Если ни один элемент управления не вызывает этот метод для указания наличия в нем секретной информации, шифрование к состоянию просмотра не применяется, что исключает появление возникающих при шифровании непроизводительных издержек.

С другой стороны, элемент управления не контролирует ситуацию полностью: если он вызывает метод Page.RegisterRequiresViewStateEncryption(), а для режима шифрования указано значение Never, данные состояния просмотра не будут шифроваться.

При хешировании и шифровании данных ASP.NET использует специфический ключ данного компьютера, определенный в разделе <machineKey> файла machine.config, который описывался в главе 5. По умолчанию увидеть фактическое определение для раздела <machineKey> не получится, потому что оно инициализируется программно. Однако, эквивалентное содержимое можно будет найти в файлах machine.config.comments. При желании вы сможете добавить туда элемент <machineKey> и там настроить его параметры.

СОВЕТ

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

Передача информации

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

Строка запроса

Наиболее распространенный подход – передавать информацию с помощью строки запроса в URL-адресе. Именно этот подход часто применяется в поисковых службах. Например, при выполнении поиска на Web-сайте Google, вы будете перенаправлены на новый URL-адрес, включающий указанные вами параметры поиска:

http://www.google.ca/search?q=organic+gardening

Строка запроса – это та часть URL-адреса, которая находится после вопросительного знака. В данном случае, она определяет одну единственную переменную под названием “ask”, которая содержит строку “organic+gardening”.

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

И все-таки добавление информации в строку запроса является полезной технологией. Она особенно подходит для приложений баз данных, в которых пользователю отображается список элементов, соответствующих записям в базе данных (например, это может быть список продуктов): пользователь выбирает элемент, после чего он перенаправляется на другую страницу, содержащую детальную информацию о том элементе, который был им выбран. Один из наиболее простых способов реализовать такую схему – это заставить первую страницу отправлять идентификатор элемента второй странице. Вторая страница затем будет отыскивать этот элемент в базе данных и отображать детальную информацию о нем. Эта технология очень часть применяется на сайтах электронной коммерции, таких как Amazon.com.

Использование строки запроса

Чтобы сохранить информацию в строке запроса, ее придется сначала самостоятельно поместить туда. К сожалению способа сделать это с помощью коллекций не существует. Как правило это означает, что придется использовать специальный элемент управления HyperLink или оператор Response.Redirect(), подобный тому, который показан ниже:

// Переходим на страницу newpage.aspx. Предоставляем/отправляем один единственный аргумент // строки запроса под названием recordID и указываем для него значение 10.
int recordID = 10;
Response.Redirect("newpage.aspx?recordID=" + recordID.ToString());

Отправлять можно и несколько параметров, только тогда их следует разделять с помощью амперсанда (&), как показано ниже:

// Переходим на страницу newpage.aspx. Предоставляем два аргумента в строке 
// запроса: номер записи (10) и режим(full).
Response.Redirect("newpage.aspx?recordID=10&mode=full");

Получающей странице легче работать со строкой запроса. Она может получать значения из коллекции типа словаря QueryString, предоставляемой встроенным объектом Request, как показано ниже:

string ID = Request.QueryString["recordID"];

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

ПРИМЕЧАНИЕ

К сожалению, ASP.NET не предоставляет никакого механизма для автоматической проверки или шифрования данных строки запроса. Он мог бы работать практически точно так же, как и механизм защиты состояния просмотра. Без этих функциональных возможностей, строка запроса очень уязвима. В главе 25 мы более подробно рассмотрим классы шифрования и покажем, как с их помощью можно создать по-настоящему безопасную строку запроса.

URL-Кодировка URL-адреса

Одной из потенциальных проблем со строкой запроса является использование в ней символов, которые не разрешается использовать в URL-адресе. Список символов, которые разрешено использовать в URL-адресе намного короче списка символов, которые разрешено использовать в HTML-документе. В целом, в URL-адресе могут использоваться только буквы, цифры или такие специальные символы, как: $, -,_, ., +, !, *, ‘, (), ,. Некоторые браузеры допускают наличие в URL-адресе некоторых дополнительных символов (самым нестрогим в этом плане является Internet Explorer), но многие все-таки нет.

При подозрении, что сохраняемые в строке запроса данные могут включать недопустимые для URL-адреса символы, следует применить URL-кодировку. Когда применяется URL-кодировка, специальные символы заменяются последовательностями преобразованных символов, начинающимися со знака процентов (%), за которым следует двузначное шестнадцатиричное представление. Например, символ пробела будет выглядеть так: %20.

Используя методы класса HttpServerUtility, данные можно закодировать автоматически. Например, ниже показано, как можно было бы закодировать строку произвольных данных для использования в строке запроса. Этот код просто заменяет все недопустимые символы последовательностями преобразованных символов.

string productName = "Flying Carpet";
Response.Redirect("newpage.aspx?productName=" + Server.UrlEncode(productName));

А вот как можно было бы декодировать эту же информацию:

string ID = Server.UrlDecode(Request.QueryString["recordID"]);

Пересылка данных между страницами

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

Инфраструктура, которая поддерживает межстраничную отправку данных – это новое свойство, получившее название PostBackUrl. Это свойство определяется интерфейсом IButtonControl и появляется в элементах управления типа кнопки, таких как: ImageButton, LinkButton и Button. Чтобы использовать межстраничную пересылку данных, необходимо просто присвоить свойству PostBackUrl в качестве значения имя другой Web-формы. Когда пользователь щелкнет на кнопке, страница будет отправлена по этому новому URL-адресу вместе со всеми значениями, которые на текущий момент содержаться в ее элементах управления.

Показанный ниже в качестве примера фрагмент кода определяет форму с двумя текстовыми полями и кнопкой, которая выполняет отправку данных на страницу под названием CrossPage2.aspx:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="CrossPage1.aspx.cs"
   Inherits="CrossPage1" %>
<html>
<head runat="server">
   <title>CrossPage1</title>
</head>
<body>
   <form id="form1" runat="server" >
    <div>
     <asp:TextBox runat="server" ID="txtFirstName"></asp:TextBox> &nbsp;
     <asp:TextBox runat="server" ID="txtLastName"></asp:TextBox>
     <asp:Button runat="server" ID="cmdSubmit"
      PostBackUrl="CrossPage2.aspx" Text="Submit" />
   </div>
   </form>
</body>
</html>

Страница CrossPage2.aspx может взаимодействовать с объектами страницы CrossPage1.aspx, используя свойство Page.PreviousPage. Например:

protected void Page_Load(object sender, EventArgs e)
{
   if (PreviousPage != null)
   {
      lblInfo.Text = "You came from a page titled " +
        PreviousPage.Header.Title;
   }
}

Обратите внимание на то, что эта страница, прежде чем пытаться получить доступ к объекту PreviousPage, выполняет проверку на наличие ссылки null. Если объекта PreviousPage не существует, никакая межстраничная пересылка данных не выполняется.

ASP.NET несколько хитрым образом заставляет эту систему работать. Когда вторая страница впервые пытается получить доступ к объекту Page.PreviousPage, ASP.NET требуется создать этот объект предыдущей страницы. Чтобы сделать это, ASP.NET фактически запускает процесс обработки страницы, но прерывает его прямо перед началом этапа PreRender. Попутно ASP.NET создает объект-заместитель Response, который незаметно перехватывает и игнорирует все поступающие с предыдущей страницы команды Response.Write(). Однако, имеются еще некоторые интересные побочные эффекты. Например, для кнопки, инициировавшей отправку данных (если она определена) срабатывают все события предыдущей страницы, включая событие Page.Load, событие Page.Init и даже событие Button.Click. Срабатывание этих событий является обязательным, потому что они необходимы для правильной инициализации страницы. Трассировочные сообщения в отличие от сообщений Response не игнорируются, что означает, что при межстраничной отправке данных можно просматривать трассировочную информацию с обеих страниц.

Получение/извлечение специфической информации о странице

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

Ниже показан пример того, как это следует делать, в котором сначала выполняется проверка на то, является ли объект PreviousPage экземпляром ожидаемого источника (CrossPage1):

protected void Page_Load(object sender, EventArgs e)
{
   if (PreviousPage != null)
   {
      CrossPage1 prevPage = PreviousPage as CrossPage1;
      if (prevPage != null)
      {
         // (Считываем какую-нибудь информацию с предыдущей страницы.)
      }
   }
}

Эту проблему можно решить и другим способом. Вместо того, чтобы вручную приводить ссылку к соответствующему типу, можно добавить в код страницы директиву элемента управления PreviousPageType, которая обозначает ожидаемый тип страницы, инициирующей межстраничную пересылку данных. Например:

<%@ PreviousPageType VirtualPath="CrossPage1.aspx" %>

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

СОВЕТ

Увидев, что свойство PostBackUrl может указывать только на одну страницу, можно решить, что процесс межстраничной пересылки данных может быть налажен только между двумя страницами. Однако, это не так: вы запросто можете расширять эту модель с помощью различных технологий. Например, вы можете изменить свойство PostBackUrl программно, чтобы/и выбрать другую целевую страницу. Или наоборот сделать так, чтобы целевая страница, инициирующая межстраничную пересылку данных, могла проверять значение свойства PreviousPage и определять, относится ли она к какому-нибудь из нескольких различных классов. А затем, вы можете выполнять различные задачи в зависимости от того, какая страница инициировала межстраничную пересылку данных.

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

public TextBox FirstNameTextBox
{
   get { return txtFirstName; }
}
public TextBox LastNameTextBox
{
   get { return txtLastName; }
}

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

Более удачный вариант – определить специфические, ограниченные методы, которые будут извлекать только необходимую информацию. Например:

public string GetFullName()
{
   get { return txtFirstName.Text + txtLastName.Text; }
}

При применении такого подхода отношения между страницами являются хорошо задокументированными и не представляют сложности для понимания. В случае изменения элементов управления на исходной странице, интерфейс для общедоступных методов скорее всего удастся сохранить. Например, если бы мы изменили элементы управления, отвечающие за получение информации об имени в предыдущем примере, нам бы конечно пришлось вносить поправки в код метода GetFullName(). Однако, после внесения всех этих необходимых изменений на странице CrossPage1.aspx, на странице CrossPage2.aspx нам абсолютно ничего изменять было бы не нужно.

СОВЕТ

В большинстве случаев хорошей альтернативой межстраничной пересылки данных является использование элемента управления, имитирующего множество страниц или множество шагов типа отдельных элементов управления Panel, элемента управления MultiView или элементу управления Wizard. Такой подход обеспечивает практически точно такой же конечный результат для пользователя и упрощает модель кодирования.

Выполнение межстраничной пересылки данных в любом обработчике событий

Как рассказывалось в предыдущем разделе, межстраничная пересылка данных доступна только с элементами управления, которые реализуют интерфейс IButtonControl. Однако, существует “обходной путь”: можно использовать перегруженную версию метода Server.Transfer() для перехода на новую ASP.NET-страницу, при этом информация состояния просмотра остается нетронутой. Для этого необходимо просто включить в этот метод булев параметр preserveForm и установить для него значение true, как показано ниже:

Server.Transfer("CrossPage2.aspx", true);

Это дает возможность использовать межстраничную пересылку данных где угодно в коде Web-станицы.

Интересно то, что существует способ, позволяющий различать, когда межстраничная отправка данных инициируется непосредственно через кнопку, а когда – через метод Server.Transfer(). Хотя получить доступ к объекту Page.PreviousPage можно в обоих случаях, если используется метод ServerTransfer(), свойство Page.PreviousPage.IsCrossPagePostBack будет иметь значение false. Ниже показан псевдокод, позволяющий понять, о чем идет речь:

if (PreviousPage == null)
{
   // Страница была запрошена (или отправлена обратно)напрямую.
}
else if (PreviousPage.IsCrossPagePostBack)
{
   // Межстраничная пересылка данных, инициируемая кнопкой.
}
else
{
   // Передача данных с сохранением информации о состоянии через метод Server.Transfer().
}

Межстраничная пересылка и проверка данных

При использовании технологии межстраничной пересылки данных с элементами управления типа Validator (которые описывались в главе 4) возникает несколько сложностей. Как рассказывалось в главе 4, в случае использования элементов управления типа Validator обязательно должна выполняться проверка значения свойства Page.IsValid, гарантирующая, что введенные пользователем данные являются корректными. Хотя возможность отправки пользователями недействительных страниц обратно серверу обычно исключается (путем написания какого-нибудь специального клиентского сценария JavaScript), это далеко не всегда так. Например, браузер клиента может просто не поддерживать JavaScript, или же какой-нибудь злонамеренный пользователь может отыскать способ “обходить” все выполняющиеся на стороне клиента проверки на правильность вводимой информации.

Когда проверка на корректность используется в сценарии с межстраничной пересылкой данных, всегда существует вероятность возникновения каких-нибудь проблемы. Давайте посмотрим, что же именно происходит, когда применяется технология межстраничной пересылки данных, и исходная страница включает элементы управления типа Validator? На рис. 6.4 показан пример страницы, которая включает элемент управления RequiredFieldValidator, требующий ввода данных в текстовом поле.


Рис. 6.4. Использование элемента управления типа Validator на странице, выполняющей перекрестную пересылку данных

Если вы щелкните на одной из кнопок, чтобы выполнить межстраничную отправку данных (для свойства CausesValidation обеих из которых установлено значение true), вам не позволит это сделать выполняющийся в браузере клиентский сценарий, отвечающий за проверку правильности вводимых данных. Вместо этого появится сообщение об ошибке. А чтобы посмотреть, что происходит, когда клиентский сценарий не поддерживается браузером, установите для свойства RequiredFieldValidator.EnableClientScript значение false. (Вы сможете позже изменить его обратно на true, как только откорректируете свой код). Теперь, если вы щелкните на одной из кнопок, данная страница будет отправлена обратно серверу и появится новая страница.

Чтобы исключить такое поведение, обязательно, прежде чем выполнять какие-либо другие операции, на целевой странице следует выполнить проверку правильности исходной страницы с помощью свойства Page.IsValid. Это стандартный метод защиты, используемый в любой Web-форме, которая реализует проверку правильности данных. Разница состоит лишь в том, что если данные на странице являются недействительными, просто ничего не делать будет недостаточно. Здесь придется выполнить еще один дополнительный шаг: вернуть пользователя на исходную страницу. Вот какой код для этого понадобится:

protected void Page_Load(object sender, EventArgs e)
{
   if (PreviousPage != null)
   {
      if (!PreviousPage.IsValid)
      {
         // Отображаем сообщение об ошибке или просто ничего не делаем.
      }
      else
      { ... }
   }
}

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

if (!PreviousPage.IsValid)
{
   Response.Redirect(Request.UrlReferrer.AbsolutePath + "?err=true");
}

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

protected void Page_Load(object sender, EventArgs e)
{
   if (Request.QueryString["err"] != null)
     Page.Validate();
}

Можно было бы попытаться еще больше улучшить эту страницу. Например, когда речь идет об очень подробной форме, которую пользователь уже наполовину заполнил, повторное запрашивание страницы – не очень хорошая идея, потому что оно приведет к “очищению” (удалению) содержимого всех элементов управления, и пользователю придется начинать все сначала. Вместо этого, пожалуй было бы лучше включить в поток ответа небольшой JavaScript-код, который мог бы использовать функцию Back (Назад) браузера для возврата на исходную страницу. Более подробную информацию о JavaScript можно найти в главе 29.

Специальные cookie-наборы

Специальные cookie-наборы – это еще один способ сохранить информацию для последующего использования. Cookie-наборы представляют собой небольшие файлы, которые создаются на жестком диске клиента (или, если они являются временными, в памяти Web-браузера). Одним из преимуществ cookie-наборов является то, что они работают “прозрачно” и пользователь даже не знает о том, что эта информация должна быть сохранена. Они также могут запросто использоваться любой страницей в приложении и даже сохраняться между посещениями, что подразумевает действительно долгосрочное хранение. Они имеет/страдают от некоторые те же недостатки, что и строки запросов. А именно: они могут хранить только простую строковую информацию, и пользователь запросто может получить к ним доступ и просмотреть их, отыскав и открыв соответствующий файл. Эти факторы делают их неподходящим вариантом, когда требуется сохранить сложную или секретную информацию или просто большие объемы данных.

Некоторые пользователи отключают функцию, отвечающую за cookie-наборы, в своих браузерах, что приводит к появлению проблем при работе с Web-приложениями, которые требуют их. В целом, cookie-наборы можно назвать распространенным явлением, потому что так много сайтов используют их. Однако, они могут ограничивать потенциальную аудиторию и никак не подходят для браузеров, встроенных в мобильные устройства.

Прежде чем использовать cookie-наборы, следует импортировать пространство имен System.NET, которое позволяет работать с соответствующими типами, как показано ниже:

using System.Net;

В использовании cookie-наборов в принципе нет ничего особого сложного. Коллекцию Cookies предоставляют два объекта: объект Request и объект Response (оба из которых предоставляются через свойства объекта Page). Главное запомнить следующее: извлекать cookie-наборы следует из объекта Request, а устанавливать их следует с помощью объекта Response.

Чтобы установить cookie-набор, просто создайте новый объект System.Net.HttpCookie. Затем, заполните его строковой информацией (используя уже знакомый словарный шаблон/модель типа словаря) и присоедините его к текущему Web-ответу, как показано ниже:

// Создаем объект cookie-набора.
HttpCookie cookie = new HttpCookie("Preferences");

// Устанавливаем в нем значение.
cookie["LanguagePref"] = "English";

// Добавляем его в текущий Web-ответ.
Response.Cookies.Add(cookie);

Cookie-набор, добавленный таким образом, будет сохраняться до тех пор, пока пользователь не закроет окно браузера, и будет отправляться вместе с каждым запросом. Чтобы создать cookie-набор более долгого срока хранения/с более длинным сроком хранения, установите дату истечения срока, как показано ниже:

// Этот cookie-набор будет оставаться действительным/храниться в течение одного года.
cookie.Expires = DateTime.Now.AddYears(1);

Извлекаются cookie-наборы по имени с помощью коллекции Request.Cookies, как показано ниже:

HttpCookie cookie = Request.Cookies["Preferences"];

// Проверяем, был ли найден cookie-набор с таким именем.
// Это хорошая мера предосторожности, потому что
// пользователь может отключить поддержку cookie-наборов, а
// в таком случае cookie-набор не будет существовать.
string language;
if (cookie != null)
{
   language = cookie["LanguagePref"];
}

Единственный способ удалить cookie-набор – это заменить его на cookie-набор, дата истечения срока хранения которого уже прошла. Следующий фрагмент кода демонстрирует, как это можно сделать:

HttpCookie cookie = new HttpCookie("LanguagePref");
cookie.Expires = DateTime.Now.AddDays(-1);
Response.Cookies.Add(cookie);
ПРИМЕЧАНИЕ

Вы заметите, что некоторые другие технологии ASP.NET тоже предполагают использование cookie-наборов, среди них: технология под названием “Состояние сеанса” (которая позволяет временно сохранять специфическую информацию о пользователе в памяти сервера) и технология под названием “Безопасность форм” (которая позволяет ограничивать доступ к определенным разделам Web-сайта и заставлять пользователей получать к ним доступ через страницу регистрации)

Состояние сеанса

Состояние сеанса – это самая сложная технология управления состояниями. Она позволяет сохранять информацию на одной странице и затем получать к ней доступ с другой страницы, а также поддерживает объекты любого типа, включая специальные, создаваемые самим разработчиком, типы данных. Лучше всего то, что состояние сеанса использует тот же основанный на коллекциях синтаксис, что и состояние просмотра. Единственное отличие - имя встроенного свойства страницы, которое в данном случае выглядит так: Session.

Каждый клиент, который получает доступ к приложению, имеет свой сеанс и свою отличную (отдельную) коллекцию данных. Состояние сеанса идеально подходит для сохранения таких данных, как элементы, которые находятся в “корзине для покупок” пользователя, когда он переходит с одной страницы на другую. Но использование состояния сеанса имеет свою цену. Хотя оно решает многие из проблем, которые возникают в случае применения других технологий управления состояниями, его использование вынуждает Web-сервер сохранять дополнительную информацию в памяти. Эта необходимость в использовании дополнительных ресурсов памяти сервера, пусть даже в маленьком объеме, очень быстро может достичь угрожающего производительности уровня, когда к сайте начнут получать доступ сотни или тысячи клиентов.

Архитектура сеанса

Управление сеансом не является частью HTTP-стандарта. Поэтому ASP.NET приходится выполнять некоторую дополнительную работу, чтобы отследить информацию сеанса и привязать ее к соответствующему ответу.

ASP.NET отслеживает каждый сеанс с помощью уникального 120-ти битового идентификатора. ASP.NET использует для генерации этого значения оригинальный (патентованный) алгоритм, что, согласно статистике, обеспечивает гарантию того, что число будет уникальным и достаточно случайным для того, чтобы злонамеренный пользователь не смог воссоздать или угадать идентификатор сеанса, которым будет пользоваться данный клиент. Этот идентификатор является единственным фрагментом информации, который передается между Web-сервером и клиентом. Когда клиент предоставляет идентификатор сеанса. ASP.NET отыскивает соответствующий сеанс, извлекает из сервера состояний “сериализованные” (т.е. хранящиеся там в сериализированном виде ) данные, преобразовывает их в “реальные” объекты и помещает эти объекты в специальную коллекцию для того, чтобы к ним можно было получить доступ в коде/чтобы они были доступны в коде. Весь этот процесс выполняется автоматически.

ПРИМЕЧАНИЕ

При каждом новом запросе ASP.NET генерирует новый идентификатор сеанса до тех пор, пока состояние сеанса не будет фактически использовано для сохранения какой-нибудь информации. Такое поведение позволяет немного повысить производительность. Коротко это можно объяснить так: зачем тратить время на сохранение идентификатора сеанса, если он не используется?

На данном этапе читателя наверняка интересуют следующие вопросы: где ASP.NET хранит данные сеанса и как выполняется их сериализация и десериализация. В классической версии ASP, состояние сеанса реализуется/-вано в виде COM-объекта со свободными потоками, который содержится в библиотеке asp.dll. В ASP.NET интерфейс программирования является практически идентичным, но вот лежащая в его основе реализация достаточно сильно отличается.

Как читатель видел в главе 5, когда ASP.NET обрабатывает HTTP-запрос, тот проходит через конвейер различных модулей, которые могут реагировать на события приложения. Одним из модулей в этой цепочке является модуль SessionStateModule (который находится в пространстве имен System.Web.SessionState). Этот модуль генерирует идентификатор сеанса, извлекает из внешних поставщиков состояния данные сеанса и затем привязывает эти данные к контексту вызовов запроса. Он также сохраняет данные состояния сеанса, когда обработка страницы завершается. Однако, важно понимать, что модуль SessionStateModule фактически не хранит данные сеанса. Вместо этого, состояние сеанса сохраняется во внешних компонентах, которые называются поставщиками состояния. Весь этот процесс показан на рис. 6.5.


Рис. 6.5. Архитектура состояния сеанса в ASP.NET

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

И, наконец, так сказать последним фрагментом в этой головоломке является то, как cookie-набор отслеживается от одного запроса к следующему. Чтобы состояние сеанса работало, клиент вместе с каждым запросом должен предоставлять соответствующий идентификатор сеанса. Добиться этого можно двумя способами:

О том, как конфигурируются не поддерживающие cookie-наборов сеансы и различные поставщики состояния более подробно будет рассказываться в разделе “Конфигурирование состояния сеанса”.

Использование состояния сеанса

Взаимодействовать с состоянием сеанса можно используя класс System.Web.SessionState.HttpSessionState, который на Web-странице ASP.NET доступен в виде встроенного объекта Session. Синтаксис для добавления элементов в эту коллекцию и их извлечения выглядит практически точно так же, как и синтаксис, который используется для добавления элементов в состояние просмотра страницы.

Например, сохранить объект DataSet в памяти сеанса можно следующим образом:

Session["ds"] = ds;

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

ds = (DataSet)Session["ds"];

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

В первых двух случаях, сеанс фактически остается в памяти, потому что Web-сервер не имеет ни малейшего понятия о том, что клиент закрыл или сменил окно браузера. Сеанс будет находиться в памяти, оставаясь недоступным, до тех пор пока в конце концов не истечет срок его действия.

Кроме того, состояние сеанса также будет утрачиваться и в случае воссоздания домена приложения. Это процесс происходит автоматически, когда разработчик обновляет свое Web-приложение или изменяет какие-нибудь конфигурационные настройки. Домен приложения также может создаваться заново через определенные промежутки времени для гарантии нормальной работы данного приложения, о чем более подробно будет рассказываться в главе 18. Если такое поведение причиняет проблемы, состояние сеанса можно сохранить за пределами процесса, как описывается в следующем разделе. Когда состояние сохраняется за пределами процесса, данные сеанса не утрачиваются даже при отключении домена приложения.

В таблице 6.4 перечислены все методы и свойства класса HttpSessionState.

ЧленОписание
CountКоличество элементов в коллекции текущего сеанса.
IsCookielessSessionОпределяет/указывает, как отслеживается этот сеанс: через/с помощью cookie-набора или с помощью измененных URL-адресов.
IsNewSessionОпределяет/указывает, был ли данный сеанс создан только что для текущего запроса. Если в состоянии сеанса на текущий момент не содержится никакой информации, ASP.NET не будет беспокоиться ни об отслеживании сеанса, ни о создании cookie-набора сеанса. Вместо этого, сеанс будет воссоздаваться заново при каждом запросе.
ModeПредоставляет значение, которое объясняет, как ASP.NET хранит информацию о состоянии сеанса. Этот режим хранения определяется на основе указанных в файле web.config конфигурационных настроек, которые будут рассматриваться чуть позже в этой главе.
SessionIDПредоставляет строку с уникальным идентификатором сеанса для текущего клиента.
StaticObjectsПредоставляет коллекцию элементов сеанса “только для чтения”, которые были объявлены в global.asax с помощью дескрипторов <object runat=server>. Вообще эта технология не используется и является пережитком ASP-программирования; применяется для обратной совместимости.
TimeoutТекущее количество минут, которое должно пройти прежде, чем текущий сеанс будет завершен при условии отсутствия запросов от клиента. Это значение может изменяться программным путем, что дает возможность при необходимости продлевать срок жизни коллекции сеанса для более важных операций.
Abandon()Незамедлительно отменяет текущий сеанс и освобождает все занимаемые им ресурсы памяти. Такая технология полезна на автономных страницах, поскольку позволяет освобождать ресурсы памяти сервера настолько быстро, насколько возможно.
Clear()Удаляет все элементы сеанса, но не изменяет идентификатора текущего сеанса.
Таблица 6.4. Члены класса HttpSessionState

Конфигурирование состояния сеанса

Cконфигурировать состояние сеанса можно с помощью элемента <sessionState> в файле web.config. Ниже вкратце перечислены все доступные параметры настройки, которые можно использовать:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <system.web>
      <!—Остальные параметры настройки были опущены. -->

      <sessionState
         mode="InProc"
         stateConnectionString="tcpip=127.0.0.1:42424" stateNetworkTimeout="10"
         sqlConnectionString="data source=127.0.0.1;Integrated Security=SSPI"
         sqlCommandTimeout="30" allowCustomSqlDatabase="false"
         useHostingIdentity="true"
         cookieless="UseCookies" cookieName="ASP.NET_SessionId"
         regenerateExpiredSessionId="false"
         timeout="20"
         customProvider=""
      />
   </system.web>
</configuration>

Более подробно атрибуты сеанса описываются уже в следующих разделах.

Mode

Параметр настройки состояния сеанса Mode позволяет указывать, какой поставщик состояния сеанса должен использоваться для хранения данных состояния сеанса между запросами. Допустимые значения описываются ниже.

Off

Установка значения Off приводит к отключению функции управления состоянием сеанса для всех страниц в приложении. Это может немного повысить производительность Web-сайтов, которые не используют состояние сеанса.

InProc

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

Значение InProc используется по умолчанию и подходит для большинства Web-сайтов небольшого размера. Однако, в сценарии с группой серверов от него не будет никакого толку. Сделать так, чтобы состояние сеанса могли совместно использовать сразу несколько серверов, можно только воспользовавшись внепроцессным поставщиком или службой состояний SQL Server. Еще одна причина, по которой установка значения InProc может быть нежелательной, состоит в том, что оно подразумевает создание более “хрупких” сеансов. В ASP.NET, домены приложений нередко создаются заново в ответ на различные операции типа изменения конфигурационных настроек или обновления страниц, а так же при достижении определенных пороговых значений (независимо от того, произошла ошибка или нет). Если вы обнаружите, что домен вашего приложения часто перезапускается, что приводит к преждевременному завершению/утрате сеансов, вы можете попытаться устранить этот эффект, изменив те или иные параметры модели процесса (см. главу 18), или воспользоваться другим более надежным поставщиком состояния сеанса.

Прежде чем использовать внепроцессную модель (StateServer) или службу состояний SQL Server, придется принять во внимание следующие моменты:

StateServer

В случае установки этого значения, ASP.NET будет использовать для управления состоянием отдельную службу Windows. Даже при запуске на том же самом Web-сервере эта служба будет загружаться за пределами основного процесса ASP.NET, что дает ей базовый уровень защиты/что обеспечивает для нее базовый уровень защиты, когда возникает необходимость перезапустить процесс ASP.NET/когда процессу ASP.NET необходимо перезапуститься. Недостатком такого подхода является то, что из-за того, что данные состояния передаются между двумя процессами, увеличивается время задержки. Если доступ к данным сеанса получается часто и они часто изменяются, это может сильно замедлить работу.

Выбрав режим StateServer, обязательно следует указать значение для параметра stateConnectionString. Эта строка сообщает TCP/IP-адрес компьютера, на котором запускается служба StateServer, и номер его порта (который определяется ASP.NET и который как правило не требуется изменять). Это позволяет обслуживать службу StateServer на другом компьютере. Если не изменить значение этого параметра, будет использоваться локальный сервер (адрес которого выглядит так: 127.0.0.1).

Конечно, прежде чем приложение сможет использовать службу, ее придется запустить. Самый простой способ сделать это – воспользоваться консолью управления Microsoft (Microsoft Management Console). Выберите в меню Start (Пуск) пункт Programs ( Administrative Tools ( Computer Management (Все программы ( Администрирование ( Управление компьютером) или же отобразите панель управления, щелкните на значке Administrative Tools (Администрирование), а затем – на значке Computer Management (Управление компьютером). В появившемся диалоговом окне Computer Management (Управление компьютером) разверните узел Services and Applications (Службы и приложения) и щелкните на элементе Services (Службы). В правой части окна появится список служб: отыщите в нем службу под названием “ASP.NET State Service”, как показано на рис. 6.6.


Рис. 6.6. Служба состояний ASP.NET

Отыскав службу в списке, вы можете вручную запустить или остановить ее с помощью щелчка правой кнопкой мыши. Скорее всего вы захотите сконфигурировать Windows так, чтобы эта служба запускалась автоматически. Чтобы сделать это, щелкните на ней правой кнопкой мыши, в появившемся контекстном меню выберите пункт Properties (Свойства) и в списке Startup Type (Тип запуска) выберите значение Automatic (Авто), как показано на рис. 6.7.


Рис. 6.7. Свойства службы

ПРИМЕЧАНИЕ

Используя режим StateServer, вы также можете установить значение для необязательного атрибута stateNetworkTimeout, указывающего максимальное количество секунд, в течение которых должен ожидаться ответ от службы, прежде чем запрос будет отменен. По умолчанию это значение равняется 10 секундам.

SqlServer

Это значение указывает ASP.NET использовать для хранения данных сеанса базу данных SQL Server, применяя параметры, определенные в атрибуте sqlConnectionString. Такой способ управления состоянием является самым удобным, но и пока что самым медленным. Чтобы его можно было использовать, на сервере должна быть установлена база данных SQL Server.

Установка значения для атрибута sqlConnectionString выполняется по схеме, подобной той, что используется для получения доступа к данным ADO.NET (и которая будет описываться в части II). В целом она/это подразумевает указание источника данных (т.е. адреса сервера), имени пользователя и пароля, если только не используется интегрированная система безопасности SQL.

Помимо этого, также должны быть установлены специальные хранимые процедуры и временные базы данных сеансов. Эти хранимые процедуры будут отвечать за сохранение и извлечение данных сеанса. В ASP.NET для этой цели имеется сценарий типа Transact-SQL, который называется “InstallSqlState.sql”. Он находится в каталоге с:\[Каталог Windows]\ Microsoft.NET\ Framework\[Версия]. Запустить его можно с помощью утилиты SQL Server, такой как OSQL.exe или Query Analyzer (Анализатор запросов). Его достаточно выполнить всего лишь один раз. Если возникнет необходимость изменить службу состояний, можно будет воспользоваться сценарием UninstallSqlState.sql и удалить таблицы состояний/-ния?.

Тайм-аут состояния сеанса .. Это связано с тем, что сценарий InstallSqlState также создает на сервере/для сервера SQL Server новое здание под названием “ASPState_Job_DeleteExpiredSessions”. Пока работает служба SQLServerAgent, это задание будет выполняться каждую минуту.

Кроме того, таблицы состояния будут удаляться при каждом перезапуске сервера SQL Server, независимо от установленного лимита времени сеанса. Это связано с тем, что когда используется сценарий InstallSqlState, таблицы состояния создаются в базе данных tempdb, которая представляет собой временное хранилище. Если такое поведение не подходит, вместо сценариев InstallSqlState.sql и UninstallSqlState.sql можно использовать сценарии InstallPersistSqlState.sql и UninstallPersistSqlState.sql. В этом случае таблицы состояния будут создаваться в базе данных ASPState и являться постоянными (а не временными).

Обычно база данных состояния всегда называется “ASPState”. Поэтому строка подключения в файле web.config не отображает явно имя базы данных. Вместо этого, она просто отражает месторасположение сервера и тип аутентификации, который будет использоваться:

<sessionState sqlConnectionString="data source=127.0.0.1;Integrated Security=SSPI"
... />

При желании использовать другую базу данных (с такой же структурой), просто установите для атрибута allowCustomSqlDatabase значение true и убедитесь в том, что строка подключения включает параметр Initial Catalog, указывающий имя базы данных, которую следует использовать:

<sessionState allowCustomSqlDatabase="false" sqlConnectionString=
"data source=127.0.0.1;Integrated Security=SSPI;Initial Catalog=CustDatabase"
... />

Выбрав режим SqlServer, вы также можете установить значение для необязательного атрибута sqlCommandTimeout. Этот атрибут указывает максимальное количество секунд, в течение которых должен ожидаться ответ от базы данных, прежде чем запрос будет отменен/в течение которых следует ожидать ответа базы данных, прежде чем отменить запрос. По умолчанию ему присваивается значение, равное 30 секундам.

Custom

Выбрав специальный режим (Custom), вы обязательно должны указать, какой поставщик состояния сеанса должен использоваться, с помощью атрибута customProvider. Атрибут customProvider указывает на имя класса, который является частью вашего Web-приложения и находится либо в каталоге App_Code, либо в скомпилированном блоке в каталоге Bin, либо в GAC.

Создание специального поставщика состояния – это низкоуровневая задача, выполнение которой требует предельной внимательности…. Рассмотрение специальных поставщиков состояния выходит за рамки контекста данной книги. Однако, если вы хотите попытаться создать своего собственного поставщика, загляните на страницу http://weblogs.asp.net/ngur/articles/371952.aspx; там вы найдете образец.

Cookieless

Для параметра Cookieless может быть установлено одно из значений коллекции типа перечисления HttpCookieMode. Все эти значения описываются в табл. 6.5.

ЗначениеОписание
UseCookiesCookie-наборы используются всегда, даже если браузер или устройство не поддерживает их или если они были отключены. Это значение устанавливается по умолчанию. Если устройство не поддерживает cookie-наборы, информация сеанса будет утрачиваться при последующих запросах, потому что каждый запрос будет получать новый идентификатор (ID).
UseUriCookie-наборы не используются никогда, независимо от возможностей браузера или устройства. Вместо этого, идентификатор сеанса сохраняется в URL-адресе.
UseDeviceProfileASP.NET решает, какие сеансы использовать (с поддержкой cookie-наборов или без), анализируя содержимое объекта BrowserCapabilities. Недостатком такого подхода является то, что этот объект указывает, что устройство должно поддерживать: он не учитывает того факта, что пользователь мог отключить cookie-наборы в браузере, который в принципе их поддерживает. В главе 27 мы будем более подробно рассказывать от том, как ASP.NET распознает различные браузеры и определяет, поддерживают они такие функциональные возможности, как cookie-наборы, или нет.
AutoDetectASP.NET пытается определить, поддерживает браузер cookie-наборы или нет, пробуя установить и извлечь cookie-набор. Эта технология очень широко применяется в Web и позволяет точно определить, когда браузер поддерживает cookie-наборы, но возможность их использования была отключена, тем самым указывая ASP.NET использовать режим без поддержки cookie-наборов.
Таблица 6.5. Значения коллекции HtttpCookieMode

Ниже показан пример того, как можно применить не поддерживающий cookie-наборы режим (который является удобным для выполнения тестирования):

<sessionState cookieless="UseUri" ... />

В не поддерживающем cookie-наборы режиме, идентификатор сеанса будет автоматически вставляться в URL-адрес. Получив запрос, ASP.NET будет удалять идентификатор, извлекать коллекцию сеанса и направлять запрос в соответствующий каталог. Измененный URL-адрес показан ниже:

http://localhost/WebApplication/(amfvyc55evojk455cffbq355)/Page1.aspx

Поскольку идентификатор сеанса вставляется в текущий URL-адрес, все относительные связи также автоматически получают этот идентификатор сеанса. Другими словами, если пользователь на текущий момент находится на странице Page1.aspx и щелкает на относительной связи, ссылающейся на страницу Page2.aspx , эта относительная связь будет включать текущий идентификатор сеанса в виде части URL-адреса. То же произойдет и если вызвать метод Response.Redirect() с относительным URL-адресом, как показано ниже:

Response.Redirect("Page2.aspx");

Единственным настоящим ограничением не поддерживающего cookie-наборы состояния является то, что с ним нельзя использовать абсолютные ссылки/связи, потому что они не будут содержать идентификатор сеанса. Например, выполнение следующего оператора приведет к тому, что пользователь утратить все данные сеанса:

Response.Redirect("http://localhost/WebApplication/Page2.aspx");

По умолчанию, ASP.NET допускает повторное использование идентификатора сеанса. Например, если делается запрос и строка запроса содержит “просроченный” сеанс, ASP.NET создает новый сеанс и использует этот же идентификатор сеанса. Проблема состоит в том, что идентификатор сеанса может случайно появиться в каком-нибудь “общественном месте”, таком, например, как страница результатов в поисковой службе, что, в свою очередь, может закончиться следующим: многие пользователи начнут получать доступ к серверу с одним и тем же идентификатором сеанса и затем, соответственно, использовать один и тот же сеанс и одни и те же данные.

Чтобы избежать такой потенциальной угрозы безопасности, всегда, когда используете не поддерживающие cookie-наборы сеансы, включайте в свой код необязательный атрибут regenerateExpiredSessionId и устанавливайте для него значение true. В этом случае, при подключении пользователя с “просроченным” идентификатором сеанса будет генерировать новый идентификатор сеанса. Единственным недостатком является то, что этот процесс также подразумевает потерю всех данных состояния просмотра и формы на текущей странице, потому ASP.NET, дабы убедиться в том, что браузер имеет новый идентификатор сеанса, выполняет переадресацию.

СОВЕТ

Выяснить, используется ли сейчас не поддерживающий cookie-наборы сеанс, можно проверив значение свойства IsCookielessSession объекта Session.

Timeout

Еще одним важным параметром настройки состояния сеанса в файле web.config является параметр Timeout. Он указывает количество минут, в течение которых ASP.NET будет находиться в режиме ожидания (не получив запрос), прежде чем отменить сеанс.

<sessionState timeout="20" ... />

Этот параметр является одним из самых важных параметров состояния сеанса. Неправильно выбранное количество минут может значительно увеличить нагрузку на сервер и крайне негативно сказаться на производительности приложения. В идеале, выбираемые промежуток времени должен быть достаточно коротким для того, чтобы сервер мог быстро восстанавливать бесценные ресурсы памяти после того, как клиент прекращает использовать приложение, но и достаточно длинным для того, чтобы клиент мог сделать паузу и продолжить сеанс, не утратив его.

Значение параметра timeout также можно изменять программно в коде. Например, если известно, что сеанс содержит чрезвычайно большое количество информации, время хранения сеанса лучше ограничить. Для этого можно отобразить пользователю соответствующее предупредительное сообщение и затем просто изменить значение свойства Timeout. Ниже в качестве пример показана строка кода, ограничивающая время тайм-аута 10 минутами:

Session.Timeout = 10;

Обеспечение безопасности состояния сеанса

Данные в состоянии сеанса являются защищенными, потому что они хранятся исключительно на сервере. Однако, cookie-набор с идентификатором сеанса крайне уязвимы. Это означает, что какой-нибудь злонамеренный пользователь запросто может “выкрасть” cookie-набор и инициировать сеанс на другом компьютере.

Существует несколько подходов, позволяющих обойти эту проблему. Наиболее распространенный из них - использовать специальный модуль сеанса, выполняющий проверку на наличие изменений в IP-адресе клиента (пример реализации такого подхода можно найти по адресу http://msdn.microsoft.com/msdnmag/issues/04/08/WickedCode). Однако, единственным действительно безопасным подходом является разрешить получать доступ к cookie-наборам сеанса только из тех разделов Web-сайта, где используется SSL-шифрование. В таком случае cookie-набор сеанса будет шифроваться и, следовательно, становится бесполезным на других компьютерах.

Если вы решите использовать этот подход, также не забудьте пометить cookie-набор как безопасный для того, чтобы он пересылался только через SSL-соединения. Это не позволит пользователям изменять URL-адрес с https:// на http://, а значит и пересылать этот cookie-набор без SSL-шифрования. Вот какой код вам для этого нужен:

Request.Cookies["ASP.NET_SessionId"].Secure = true;

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

При использовании не поддерживающих cookie-наборов сеансов существует еще одна потенциальная угроза нарушения системы безопасности. Даже если идентификатор сеанса шифруется, сообразительный пользователь может, воспользовавшись тактикой “социальной инженерии”, вынудить другого пользователя подключиться к какому-нибудь определенному сеансу. Все, что злонамеренному пользователю нужно сделать – это “всучить” пользователю URL-адрес с действительным идентификатором сеанса. Когда пользователь щелкнет на ссылке, он подключиться к этому сеансу. Хотя с этого момента идентификатор сеанса уже защищается, хакеру теперь известно, какой идентификатор сеанса используется, и он сможет запросто “проникнуть” в этот сеанс позже.

Выполнив определенные шаги, вы можете снизить вероятность такой атаки. Во-первых, когда используете не поддерживающие cookie-наборы сеансы, всегда устанавливайте для атрибута regenerateExpiredSessionId значение true. Это не позволит злонамеренному пользователю предоставить “просроченный” идентификатор сеанса. Во-вторых, явно завершайте/отменяйте текущий сеанс, прежде чем регистрировать нового пользователя.

Состояние приложения

Состояние приложения позволяет сохранять глобальные объекты, доступ к которым может получать любой клиент. В основе состояния приложения лежит класс System.Web.HttpApplicationState, который предоставляется на всех Web-страницах через встроенный объект Application.

Состояние приложения похоже на состояние сеанса. Оно поддерживает объекты такого же типа, хранит информацию на сервере и использует такой же “словарный” синтаксис/синтаксис такого же словарного стиля. Наиболее распространенным примером применения состояния приложения является глобальный счетчик, который отслеживает, какое количество раз та или иная операция выполнялась всеми клиентами данного Web-приложения.

Например, вы могли бы создать обработчик событий global.asax, который отслеживал бы количество созданных сеансов или количество поступивших в приложение запросов. Или же вы могли использовать подобную логику в обработчике событий Page.Load для отслеживания количества раз, которое данная страница запрашивалась различными клиентами. Код, который вам следовало бы использовать в последнем случае, показан ниже:

protected void Page_Load(Object sender, EventArgs e)
{
   int count = (int)Application["HitCounterForOrderPage"];
   count++;
   Application["HitCounterForOrderPage"] = count;
   lblCounter.Text = count.ToString();
}

Опять-таки, элементы состояния приложения храняться в виде объектов, поэтому при извлечении из коллекции они должны приводиться к соответствующему типу. Срок действия/жизни элементов состояния приложения никогда не истекает. Они существуют до тех пор, пока приложение или сервер не будет перезапущен или до тех пор, пока не произойдет автоматическое обновление домена приложения (согласно установленным настройкам или в результате обновления одной из страниц или компонентов в приложении).

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

  1. Пользователь А извлекает текущее значение счетчика (432).
  2. Пользователь B извлекает текущее значение счетчика (432).
  3. Пользователь А устанавливает для счетчика значение 433.
  4. Пользователь В устанавливает для счетчика значение 433.

Другими словами, один из этих запросов не учитывается, потому что два клиента получают доступ к счетчику одновременно. Чтобы избежать этой проблемы, следует использовать методы Lock() и Unlock(), которые явно позволяют получать доступ к коллекции состояния Application только одному клиенту за раз, например так:

protected void Page_Load(Object sender, EventArgs e)
{
   // Запрашиваем монопольный доступ.
   Application.Lock();

   int count = (int)Application["HitCounterForOrderPage"];
   count++;
   Application["HitCounterForOrderPage"] = count;

   // Отключаем монопольный доступ.
   Application.Unlock();

   lblCounter.Text = count.ToString();
}

К сожалению, все остальные клиенты, запрашивающие эту страницу, теперь будут задерживаться (т.е. вынуждены будут ожидать) до тех пор, пока коллекция Application не освободиться. Это может значительно снизить производительность. Вообще, значения, которые изменяются часто, в состоянии приложения лучше не сохранять. На самом деле, состояние приложения сейчас редко используется в мире .NET, потому что в двух основных случаях, когда раньше применялась эта коллекция, теперь могут применяться более простые и более эффективные подходы:

Данные состояния приложения всегда хранятся в процессе. Это означает, использовать можно любые типы данных .NET. Однако, это также предполагает наличие тех же двух ограничений, что и у внутрипроцессного состояния сеанса, а именно: состояние приложения не может совместно использоваться группой серверов, и данные состояния приложения всегда будут утрачиваться при обновлении (перезапуске) домена приложения, которое может происходить в виде части обычного процесса обслуживания ASP.NET.

ПРИМЕЧАНИЕ

Состояние приложения поставляется главным образом для обратной совместимости с классической версией ASP. В новых приложениях для глобальных данных лучше применять другие подходы, например использовать базу данных вместе с объектом Cache.

Статические переменные приложения

Глобальные переменные приложения можно хранить еще одним способом. Вы можете добавить в файл global.asax (о котором рассказывалось в главе 5) статические переменные экземпляра, после чего они будут скомпилированы в специальный класс HttpApplication для вашего Web-приложения и станут доступными для всех страниц. Например:

public static string[] fileList;

Главная деталь, которая позволяет такому подходу работать, состоит в том, что переменная является статической. Это связано с тем, что, для того, чтобы обслуживать множество запросов, ASP.NET создает пул классов HttpApplication. Поэтому, каждый запрос может обслуживаться с помощью отдельного объекта HttpApplication, а каждый объект HttpApplication имеет свои собственные данные экземпляра. Однако, существует только одна копия статических данных, которая используется для всех экземпляров.

Еще одно требование: вы обязательно должны добавить в директиву Application атрибут ClassName, чтобы присвоить имя глобальному классу приложения, которое вам придется использовать для извлечения только что созданного статического значения.

Конечно, для наилучшей инкапсуляции (или наибольшей гибкости), вам следует использовать процедуры свойств:

private static string[] fileList;
public static string[] FileList
{
   get { return fileList; }
}

Добавляемая в файл global.asax переменная экземпляра имеет, по сути, те же характеристики, что и значение в коллекции Application. Другими словами, вы можете использовать любой тип данных.NET, значение сохраняется до перезапуска домена приложения и состояние не передается между несколькими компьютерами. Однако, механизм автоматической блокировки отсутствует. Поскольку попытаться получить доступ и изменить значение могут одновременно несколько пользователей, вам следует использовать C#-оператор блокировки, чтобы на время ограничить переменную одним единственным потоком. В зависимости от того, как осуществляется доступ к вашим данным, вы можете выполнить блокировку в коде Web-страницы (в таком случае вы сможете выполнить одновременно несколько задач с заблокированными данными) или в процедурах свойств или методах в файле global.asax (в таком случае блокировка будет удерживаться в течение наименьшего возможного времени). Ниже показан пример процедуры свойства, которая обслуживает защищенную от потоков глобальную коллекцию метаданных:

private static Dictionary<string, string> metadata =
   new Dictionary<string, string>();
public void AddMetadata(string key, string value)
{
   lock (metadata)
   {
      metadata[key] = value;
   }
}
public string GetMetadata(string key)
{
   lock (metadata)
   {
      return metadata[key];
   }
}

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

private static string[] fileList;
public static string[] FileList
{
   get
   {
      if (fileList == null)
      {
         fileList = Directory.GetFiles(
         HttpContext.Current.Request.PhysicalApplicationPath);
      }
      return fileList;
   }
}

Этот код использует классы доступа к файлам, описывавшиеся в главе 13, для извлечения списка файлов в Web-приложении. С коллекцией Application такой подход был бы невозможен.

Еще одним преимуществом применения статических переменных экземпляра является то, что использующий их код может обеспечивать типовую безопасность. Ниже показан пример такого кода, в котором используется свойство FileList:

protected void Page_Load(object sender, EventArgs e)
{
   StringBuilder builder = new StringBuilder();
   foreach (string file in Global.FileList)
   {
      builder.Append(file + "<br />");
   }
   lblInfo.Text = builder.ToString();
}

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

Заключение

Управление состоянием - это искусство сохранения информации между запросами. Как правило, эта информация касается только одного пользователя (например, это может быть список элементов в “корзине для покупок”, имя пользователя или уровень доступа), но иногда она касается всего приложения, т.е. является глобальной (например, это могут быть статистические данные об активности сайта). Поскольку ASP.NET использует несвязанную архитектуру, данные состояния должны явно сохраняться и извлекаться при каждом запросе. Подход, выбранный для сохранения этих данных, может очень сильно влиять на производительность, масштабируемость и безопасность приложения. Чтобы улучшить выбранное решение, вы практически наверняка захотите добавить в используемую схему кэширование. О том, как это лучше сделать, речь пойдет уже в главе 11.


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