Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Ajax в действии

.pdf
Скачиваний:
92
Добавлен:
01.05.2014
Размер:
6.34 Mб
Скачать

298 Часть III. Создание профессиональных Ajax-приложений

апосле либо продолжает его обычную обработку, либо появляется страница!

ссообщением об ошибке.

Листинг 7.5. Java-фильтр, используемый в системе защиты

public abstract class GenericSecurityFilter implements Filter { protected String rejectUrl=null;

public void init(FilterConfig config) throws ServletException {

//О Указание URL, соответствующего отвергнутому запросу rejectUrl=config.getInitFarameter("rejectUrl"); }

public void doFilter(

ServletRequest request, ServletResponse response, FilterChain chain)

throws IOException, ServletException {

//© Проверка допустимости запроса if (isValidRequest(request)){

chain.doFilter{request, response); }else if {rejectUrl!=null){

//0 Обращение к URL, соответствующему отвергнутому запросу RequestDispatcher dispatcher

=request.getRequestDispatcher(rejectUrl) ; dispatcher.forward(request, response); }

}

protected abstract boolean isValidRequest{ServletRequest request);

public void destroy{){}

}

щ

 

 

 

Фильтр представляет собой абстрактный класс, в котором объявлен абстрактный метод isValidRequest(). Решение о дальнейшей судьбе запроса принимается на основании его анализа. Если в результате вызова метода isValidRequest () © возвращается значение false, запрос перенаправляется по U R L ©, определенному в конфигурационном файле Web-приложения О.

Рассматриваемый здесь фильтр предоставляет возможность создавать конкретные подклассы, т.е. его можно адаптировать для различных стратегий защиты.

Использование НТТР-сеанса

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

Листинг7.6. Фильтрдля проверки идентификатора сеанса

public class SessionTokenSecurityFilter extends GenericSecurityFilter {

protected boolean isValidRequest(ServletRequest request) { boolean valid=false;

HttpSession session=request.getSession() ; if (session!=null){

UserToken token=(Token) session.getAttribute{'userToken');

Глава 7. Безопасность Ajax-приложенип

if (token!-null){ valid=true;

}

}

return valid;

}

}

я

Данный подход типичен для традиционных Web-приложений. Если проверка дает отрицательный результат, пользователю предлагается страница регистрации. В Ajax-приложениях ответ сервера может быть более простым по сравнению с традиционными Web-приложениями и содержать данные в формате XML, JSON либо в виде обычного текста. В ответ на получение определенных данных клиент может самостоятельно предоставить пользователю средства регистрации. В главе 11 мы обсудим реализацию средств регистрации в рамках приложения Ajax Portal.

Использование зашифрованных HTTP-заголовков

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

Листинг 7.7. Фильтр для проверки поля HTTP-запроса

"ШВШ!

public

class

SecretHeaderSecurityFilter

 

extends GenericSecurityFilter {

 

private String headerName=null;

 

public

void init(FilterConfig config) throws ServletException {

super.init(config);

 

 

// Имя

поля

задается

как

конфигурационный параметр

 

h e a d e r N a m e - c o n f i g . g e t l n i t P a r a m e t e r ( " h e a d e r N a m e " ) ;

 

}

 

 

 

 

 

 

 

protected

boolean

isValidRequest(ServletRequest request)

{

boolean

valid=true;

 

 

HttpServletRequest

hrequest=(HttpServletRequest)request;

if

 

(headerName!=null){

 

valid=false;

 

 

 

// О Получение значения заголовка

 

 

String

 

headerVal=hrequest.getHeader(headerName);

 

 

Encrypter

crypt=EncryptUtils.retrieve(hrequest);

 

 

if

(crypt!=null){

 

 

// © Сравнение значения заголовка

 

 

 

valid=crypt.compare(headerVal);

 

 

}

 

 

 

 

 

 

}

 

 

 

 

 

 

 

return valid;

}

^ww

часть III. иоздание профессиональных Ajax-приложений

При проверке запроса фильтр читает заголовок с определенным именем

Ои сравнивает его с кодированным значением, содержащимся на сервере

©.Данное значение не постоянно; оно генерируется для каждого конкретЛ ного сеанса, что затрудняет незаконное обращение к системе. Класс ЕпЯ

crypter использует для генерации МХ)5-значения Apache Commons Codec и javax.security.MessageDigest. Полностью набор классов можно полД чить, скопировав коды примеров для данной книги. Принцип формирования ЛГО5-значения в шестнадцатеричном формате показан ниже.

MessageDigest digest=MessageDigest.getInstance("MD5"); byte[] data=privKey.getBytes(); digest.update(data);

byte[] raw=digest.digest(pubKey.getBytes()); byte[] b64=Base64.encodeBase64(raw);

return new String{b64);

где privKey и pubKey — соответственно закрытый и открытый ключ. Чтобы настроить фильтр для проверки всех URL, соответствующих пу-

ти /Ajax/data, надо добавить к конфигурационному файлу web.xml нашего приложения следующее определение:

<filter id='securityFilter_I'> <filter-name>HeaderChecker</filter-name> <filter~class>

com.manning.aj axinaction.web.SecretHeaderSecurityFilter </filter-class>

<init-param id='securityFilter_l_param_l'> <param-name>rejectUrl</param-name> <param-value>/error/reject.do</param-value>

</init-param>

<init-param id='securityFilter_l_param_2'> <param-name>headerName</param-name> <param-value>secret-password</param-value>

</init-param> </filter>

Согласно данной конфигурации отклоненные запросы после проверки значения поля secret-password направляются URL /error/reject.do. Кроме того, мы определяем отображение фильтрации, в результате чего фильтр вступает в действие для любого запроса, соответствующего указанному пути.

<filter-mapping> <filter-name>HeaderChecker</filter-name>

<url-pattern>/ajax/data/*</url-pattern> </filter-mapping>

На стороне клиента для генерации МБ5-дайджеста Base64 использу-

ется

библиотека Пола Джонстона (она упоминалась ранее в этой гла-

ве).

Для того чтобы добавить поле HTTP-заголовка, мы вызываем ме-

тод

setRequestHeadert).

function loadXml(url){

 

var

req=null;

 

if

(window.XMLHttpRequest){

req=new XMLHttpRequest();

Глава 7. Безопасность Ajax-приложений

301

} e l s e if (window.ActiveXObject){

req=new ActiveXObject("Microsoft.XMLHTTP");

}

i f (req)f req.onreadystatechange=onReadyState; req.open(•GET',url,true);

req.setRequestHeader('secret-password',getEncryptedKey()); req.send(params);

}

}

Здесь функция кодирования создает МО5-дайджест Base64 для указанной строки.

var key="password"; function getEncryptedKey(){

return b64_md5(key);

}

Данное решение предполагает передачу Ajax-клиенту значения переменной key. Ключ для сеанса можно передать по протоколу HTTPS при регистрации пользователя. Он должен представлять собой псевдослучайное значение, а не строку символов, например.

Положительная особенность данного решения состоит в том. что поле заголовка HTTP-запроса не может быть модифицировано посредством стандартной гипертекстовой ссылки или HTML-формы. Злоумышленникам придется программировать HTTP-клиент, а это требует определенного уровня подготовки. Очевидно, что по мере роста популярности объекта XMLHttpRequest среди разработчиков информация о том, как сформировать поле заголовка в составе запроса, будет становиться все более доступной. Следует заметить, что средствами, подобными Apache HTTPClient и Perl LWP::UserAgent: эту задачу можно было решить и раньше.

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

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

И наконец, помните, что безопасность — понятие относительное. Никакая защита не может быть абсолютно надежной. Максимум, чего можно добиться — опережать на шаг злоумышленников. Применение HTTPS и проверку

302 Часть Ш. Создание профессиональных Aj'ax-приложений

HTTP-запросов в тех случаях, когда такие меры оправданы, можно рассмат, ривать как шаги в нужном направлении.

7.5.Резюме

Вданной главе обсуждались вопросы безопасности Ajax-приложений. Мы сосредоточили внимание на тех особенностях, которые отличают защиту инфраструктуры Ajax от защиты обычных Web-приложений. Сначала мы рассмотрели "песочницу" (среду для выполнения JavaScript-программ в составе Web-браузера) и правила, предотвращающие взаимодействие фрагментов кода, полученных из различных источников. Мы также рассмотрели, как можно сделать политику "сервера-источника" менее строгой и разрешить доступ к сторонним Интернет-службам, например к API Google.

Кроме того, мы рассмотрели способы защиты данных, которыми клиент обменивается с сервером. Достаточно надежную защиту обеспечивает протокол HTTPS, но существует и более простое решение, обеспечивающее безопасную передачу пароля средствами обычного протокола HTTP. И наконец, мы показали вам уязвимое место Ajax-приложений, связанное с передачей низкоуровневых данных с сервера. Поскольку в некоторых случаях это может представлять серьезную угрозу, мы рассмотрели варианты архитектуры сервера, минимизирующие опасность. Мы также обсудили средства на стороне сервера, усложняющие несанкционированный доступ к данным.

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

7.6.Ресурсы

Ключи для использования API Web-служб Google можно получить, обратившись по адресу http://www.google.com/apis/.

JavaScript-библиотеки Пола Джонстона, позволяющие создавать MD5дайджесты, представлены по адресу http://pajhome.org.uk/crypt/md5/ md5src.html. Для тех, кому хочется быстро проверить MD5 в действии, можно посоветовать URL генератора контрольных сумм (http: //www. f ilef ormat

.info/tool/hash.htm?text* ajax+in+action).

Библиотеку Apache Commons Codec для Java, которую мы использовали при генерации Base64 MD5 на сервере, можно скопировать с узла http://j akarta.apache.org/commons/codec/.

В разделе 7.1 мы рассмотрели подписание JAR-файлов для создания защищенных приложений, ориентированных на браузеры Mozilla. Дополнительная информация по этому вопросу содержится в документе http://www.mozilla.org/projects/security/components/signed-scripts. html. Сведения об игре "морской бой" доступны по адресу h t t p : / / gamesmuseum.uwaterloo.ca/vexhibit/Whitehill/Battleship/.

Производит

п

Вэтой главе...

Профилирование приложений Ajax

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

Программные решения, влияющие на производительность

Особенности работы с конкретными браузерами

часть III. Создание профессиональных Ajax-приложенип

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

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

8.1. Что такое производительность

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

Вопросы производительности никогда не следует упускать из виду. Если мы забудем о ней, пользователи вскоре забудут о нас.

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

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

Глава 8. Производительность приложения 305

дои машине, мы должны уметь заглянуть глубже спецификации DOM или стандарта ЕСМА-262, определяющего JavaScript, и учитывать реальные особенности конкретных браузеров. Если мы не будем знать "нижний уровень" иерархии программных средств, вряд ли нам удастся создать конкурентоспособный продукт. *

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

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

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

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

8.2. Скорость выполнения JavaScript-программ

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

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

. U v l o in. ^изоание профессиональных Ajax-приложений

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

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

8.2.1. Определение времени выполнения приложения

Самый простой инструмент измерения времени, имеющийся в нашем распоряжении, — это системный таймер, доступный из программы на JavaScript посредством объекта Date. Если мы создадим экземпляр Date, вызвав конструктор без параметров, то получим текущее время. Если одно значение Date вычесть из другого, разность будет представлена в миллисекундах. Пример использования объекта Date приведен в листинге 8.1.

Листинг 8.1. Измерение времени выполнения кода посредством Date

function myTimeConsumingFunctionf){ var beginning=new Date{);

// Важные и длительные вычисления

var ending=new Datef);

var duration=ending-beginning;

 

alert("this function took

"+duration

 

+"ms to do something interesting!");

_ J

 

 

Мы создаем объекты Date до начала и по окончании фрагмента кода, а затем определяем время выполнения этого фрагмента, вычисляя разность двух значений времени. В данном примере для представления информации о времени выполнения используется функция a l e r t (), но такое решение подходит лишь для простейших случаев. Обычно информацию о времени выполнения записывают в файл протокола, однако модель безопасности JavaScript запрещает доступ к локальной файловой системе. Самый простой подход, доступный в Aj axприложении, — это запись данных о профиле в память и последующее воспроизведение их в форме отчета.

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

Глава 3. Производительность приложения 307

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

var stopwatch-new Object();

//Массив зарегистрированных таймеров stopwatch.watches=new Arrayt);

//Точка входа для клиентского кода stopwatch.getWatch=function(id,startNow){

var watch-stopwatch.watches[id] ; if {!watch){

watch=new stopwatch.Stopwatch(id);

}

if (startNow){ watch.start();

}

return watch;

}

// Конструхтор объекта, выполняющего функции секундомера stopwatch.StopWatch=function(id){

this.id-id; stopwatch.watches[idl-this; this.events-new Array(); this.objViewSpec-[

{name: "count", type: "simple"}, {name: "total", type; "simple"},

{name: "events", type: "array", inline:true}

]?

)

stopwatch.Stopwatch.prototype.startefunction(){ this.current=new TimedEvent(};

}

stopwatch.Stopwatch.prototype.stop=function(){ if (this.current){

this.current.stop();

this.events.append(this.current);

this.count++;

this.total+=this.current.duration;

this.current—null;

}

)

// Конструктор обработчика события измерения времени stopwatch.TimedEvent=function(){

this.start=new Date(); this.objViewSpec=[

{name: "start", type: "simple"}, {name: "duration", type: "simple"}

];

}

stopwatch.TimedEvent.prototype.stop=function(){ var stop=new Date О; this.duration=stop-this.start;

1