Шифрование строки запроса
192ASP.NET --- Безопасность в ASP.NET --- Шифрование строки запроса
Ранее вы уже видели несколько примеров, в которых система безопасности ASP.NET работала "за кулисами", защищая ваши данные. Например при использовании аутентификации пользователей было показано, как ASP.NET использует шифрование и хеш-коды для обеспечения постоянной защиты данных в cookie-наборах. Вы также узнали, как использовать некоторые инструменты для защиты состояния представления.
К сожалению, ASP.NET не предлагает аналогичного способа для автоматического шифрования строки запроса (который представляет собой дополнительный фрагмент информации, добавляемый к URL-адресу для передачи информации от одной страницы к другой). Во многих случаях информация запроса из URL-адреса несет в себе введенные пользователем данные, и не важно то, что пользователь может видеть или модифицировать ее. В других случаях, однако, строка запроса содержит информацию, которая должна оставаться скрытой от глаз.
В этой ситуации единственный выбор состоит в переключении на другую форму управления состоянием (который может иметь другие ограничения) или же в том, чтобы заставить систему шифровать строку запроса.
В следующем примере будет продемонстрирован простой способ укрепления безопасности за счет шифрования информации перед помещением ее в строку запроса. Здесь снова можно положиться на криптографические классы .NET. Фактически, будет использоваться DPAPI.
Оболочка для строки запроса
Начальная точка - это создание класса EncryptedQueryString. Этот класс должен принимать коллекцию строковой информации (такой как строка запроса) и давать возможность извлекать ее на другой странице. "За кулисами" класс EncryptedQueryString должен шифровать данные перед помещением их в строку запроса и незаметно расшифровывать где-то на пути.
Ниже показана начальная версия класса EncryptedQueryString:
using System;
using System.Text;
using System.Web;
using System.Web.Security;
using System.Security.Cryptography;
public class EncryptedQueryString :
System.Collections.Specialized.StringDictionary
{
public EncryptedQueryString()
{
// Здесь ничего не делать
}
public EncryptedQueryString(string encryptedData)
{
// Расшифровать информацию и добавить к словарю
}
public override string ToString()
{
// Зашифровать информацию и вернуть
// в виде строки шестнадцатеричных кодов
}
}
Вы сразу должны заметить одну вещь, касающуюся класса EncryptedQueryString: он унаследован от класса StringDictionary, который представляет коллекцию строк, проиндексированную строками. За счет наследования от StringDictionary вы получаете возможность работать с EncryptedQueryString как с обычной строковой коллекцией. В результате можно добавлять информацию в EncryptedQueryString таким же образом, как это делается с коллекцией Request.QueryString.
Главное, что эта функциональность достается бесплатно, без необходимости писать какой-либо дополнительный код. Таким образом, благодаря этому рудиментарному классу, появляется возможность хранить коллекцию строк "имя-значение". Но как поместить эту информацию в строку запроса? Класс EncryptedQueryString предоставляет метод ToString(), который просматривает все данные коллекции и комбинирует их в единственную зашифрованную строку.
Первым делом, класс EncryptedQueryString должен скомбинировать отдельные значения коллекции в строку с разделителями, которую легко будет обратно превратить в коллекцию на целевой странице. В этом случае метод ToString() использует соглашения строки запроса, отделяя каждое значение от имени знаком равенства (=), а каждую пару "имя-значение" - знаком амперсанда (&). Однако чтобы это работало, следует удостовериться, что имена и значения каждого элемента коллекции не содержат специальные символы. Для решения этой проблемы ToString() использует метод HttpServerUtility.UrlEncode() для защиты строки перед ее объединением.
Ниже показана реализация метода ToString(), которая защищает и объединяет элементы коллекции в одну строку:
public override string ToString()
{
StringBuilder Content = new StringBuilder();
// Пройти no содержимому коллекции
// и построить типичную строку запроса
foreach (string key in base.Keys)
{
Content.Append(HttpUtility.UrlEncode(key));
Content.Append("=");
Content.Append(HttpUtility.UrlEncode(base[key]));
Content.Append("&");
}
// Удалить последний символ '&'
Content.Remove(Content.Length - 1, 1);
// Зашифровать содержимое, используя DPAPI
byte[] EncryptedData = ProtectedData.Protect(
Encoding.UTF8.GetBytes(Content.ToString()),
null, DataProtectionScope.LocalMachine);
// Преобразовать зашифрованный байтовый массив в допустимую строку URL.
// Это может быть подходящим местом для проверки данных на предмет
// того, что они не превышают типичного размера в 4 Кбайт
return HexEncoding.GetString(EncryptedData);
}
Здесь используется класс ProtectData для шифрования данных. Этот класс применяет DPAPI для шифрования информации и метод Protect() для возврата байтового массива, так что потребуется предпринять дополнительные шаги для преобразования байтового массива в строку, подходящую для строки запроса.
Один из подходов, который кажется разумным - применить статический метод Convert.ToBase64String(), который создает закодированную Base64 строку. К сожалению, строки Base64 могут включать символы, недопустимые для строки запроса (и именно - знак равенства). Хотя можно создать строку Base64, а потом подвергнуть ее URL-кодированию, это излишне усложнит стадию расшифровки. Проблема состоит в том, что метод ToBase64String() может также вставить серии строк, которые выглядят как URL-кодированные последовательности символов. Такие последовательности затем будут неправильно заменены в процессе декодирования.
Более простой подход предусматривает использование другой формы кодирования. В этом примере применяется шестнадцатеричное кодирование, при котором каждый символ заменяется своим алфавитно-цифровым кодом. В следующем примере показана простая реализация такого вспомогательного класса, обеспечивающего шестнадцатеричное кодирование:
public static class HexEncoding
{
public static string GetString(byte[] data)
{
StringBuilder Results = new StringBuilder();
foreach (byte b in data)
{
Results.Append(b.ToString("X2"));
}
return Results.ToString();
}
public static byte[] GetBytes(string data)
{
// GetString кодирует шестнадцатеричные числа двумя десятичными
byte[] Results = new byte[data.Length / 2];
for (int i = 0; i < data.Length; i += 2)
{
Results[i / 2] = Convert.ToByte(data.Substring(i, 2), 16);
}
return Results;
}
}
Метод GetString() просто возвращает строку с шестнадцатеричными числами, созданными из байтового массива, в то время как GetBytes() преобразует строку с шестнадцатеричными цифрами обратно в байтовый массив для дальнейшей обработки. Это очень просто реализовать, поскольку используются существующие методы преобразования, инкапсулированные в класс Convert из библиотеки .NET. Эти методы затем легко применять.
Строку, возвращенную EncryptedQueryString.ToString(), можно поместить непосредственно в строку запроса, используя метод Response.Redirect().
Целевая страница, которая принимает данные запроса, нуждается в способе десериализации и расшифровки строки. Первый шаг - создать объект EncryptedQueryString и передать зашифрованные данные. Чтобы облегчить этот шаг, имеет смысл добавить новый конструктор в класс EncryptedQueryString, который примет параметр - зашифрованную строку, как показано ниже:
public EncryptedQueryString(string encryptedData)
{
// Расшифровать переданные данные с помощью DPAPI
byte[] RawData = HexEncoding.GetBytes(encryptedData);
byte[] ClearRawData = ProtectedData.Unprotect(
RawData, null, DataProtectionScope.LocalMachine);
string StringData = Encoding.UTF8.GetString(ClearRawData);
// Расшифровать данные и добавить содержимое
int Index;
string[] SplittedData = StringData.Split(new char[] { '&' });
foreach (string SingleData in SplittedData)
{
Index = SingleData.IndexOf('=');
base.Add(
HttpUtility.UrlDecode(SingleData.Substring(0, Index)),
HttpUtility.UrlDecode(SingleData.Substring(Index + 1))
);
}
}
Конструктор первым делом декодирует шестнадцатеричную информацию из переданной строки и воспользуется DPAPI для расшифровки информации, записанной в строке запроса. Затем он разбивает информацию на части и добавляет пары "имя-значение" в базовую коллекцию StringCollection.
Теперь имеется готовая инфраструктура для создания простой тестовой страницы и безопасной передачи ее от одной страницы к другой.
Создание тестовой страницы
Чтобы протестировать класс EncryptedQueryString, понадобятся две страницы - одна, устанавливающая строку запроса и перенаправляющая пользователя, и другая, извлекающая строку запроса. Первая страница содержит текстовое поле для ввода информации, как показано ниже:
<form id="form1" runat="server">
<div>
Введите здесь какие-нибудь данные:
<asp:TextBox runat="server" ID="MyData" />
<br />
<br />
<asp:Button ID="SendCommand" runat="server" Text="Отправить данные" OnClick="SendCommand_Click" />
</div>
</form>
Когда пользователь щелкает на кнопке SendCommand, страница отправляет зашифрованную строку запроса принимающей странице следующим образом:
protected void SendCommand_Click(object sender, EventArgs e)
{
EncryptedQueryString QueryString = new EncryptedQueryString();
QueryString.Add("MyData", MyData.Text);
QueryString.Add("MyTime", DateTime.Now.ToLongTimeString());
QueryString.Add("MyDate", DateTime.Now.ToLongDateString());
Response.Redirect("QueryStringRecipient.aspx?data=" + QueryString.ToString());
}
Обратите внимание, что страница вводит полностью зашифрованную строку данных как один параметр по имени data в строку запроса страницы назначения. На рисунке ниже показана страница в действии:
Целевая страница десериализует переданную через параметр строку в параметр строки запроса с помощью ранее созданного класса:
<form id="form1" runat="server">
<div>
<h2>Извлечение информации из строки запроса:</h2>
<asp:Label runat="server" ID="QueryStringLabel" />
</div>
</form>
protected void Page_Load(object sender, EventArgs e)
{
// Десериализовать зашифрованную строку запроса
EncryptedQueryString QueryString =
new EncryptedQueryString(Request.QueryString["data"]);
// Вывести информацию на экран
StringBuilder Info = new StringBuilder();
foreach (String key in QueryString.Keys)
{
Info.AppendFormat("{0} = {1}<br>", key, QueryString[key]);
}
QueryStringLabel.Text = Info.ToString();
}
Этот код добавляет информацию в метку на странице. Результат с ранее отправленной информацией показан на рисунке ниже: