Адаптация Web-приложений к мобильным устройствам без изменения кода

Многие из существующих Web-сайтов не предназначены для мобильных устройств. Чтобы исправить это, могут потребоваться значительные инвестиции. Предлагаемая статья демонстрирует, как добавить мобильный интерфейс к существующим Web-приложениям, не изменяя код на стороне сервера. Из журнала IBM WebSphere Developer Technical Journal.

Кристиан Вот, сертифицированный ИТ-архитектор, IBM

Кристиан Вот (Christian Voth) — сертифицированный ИТ-архитектор IBM Software Services Group ― специализируется на Web-технологиях и корпоративных Java-программах. По окончании Университета Торонто он получил ученую степень по информатике и работает в лаборатории IBM в Торонто (Канада).



23.04.2013

Введение

С каждым новым выпуском Android, iOS и других мобильных операционных систем браузеры для мобильных устройств становятся все мощнее, но физические ограничения ― небольшой размер экрана и новая парадигма сенсорного ввода вместо мыши ― остаются. Хотя некоторые смартфоны в состоянии отобразить Web-сайт, результаты часто бывают неоптимальными, а то и вообще непригодными для использования. Размер кнопок и других элементов управления должен быть таким, чтобы ими было легко манипулировать пальцами, а не указателем мыши, и новые действия, такие как жесты, должны использоваться эффективно. Короче говоря, необходимы согласованные усилия, направленные на то, чтобы сделать эти сайты не только допускающими использование мобильных устройств, но и поддерживающими их.

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

Это позволяет избежать зачастую дорогостоящей необходимости развертывать изменения кода в производственной среде. А также устраняет потребность в регрессионном тестировании существующих функций, претерпевших изменения при добавлении мобильной поддержки. Кроме того, так как используются одни и те же запросы и ответы, достаточно протестировать приложение на производительность, если есть подозрение, что добавление мобильной поддержки значительно увеличит трафик. При этом подходе просто добавляются статические файлы (.html, .css, .js), которые обслуживаются из того же домена, где размещается существующее Web-приложение, и используются существующие службы, которые приложение уже предоставляет.

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

Web-приложения: немного истории

Большинство нетривиальных Web-приложений основано на понятии модель-представление-контроллер (MVC), причем контроллер расположен на сервере. Сервер поддерживает состояние сеанса и перенаправляет запросы пользователя на следующую страницу, которая должна отображаться. Каждый запрос клиента обычно вызывает разметку целой страницы, даже если при последовательных обращениях меняется только часть этой страницы. Для решения этой проблемы и повышения гибкости при агрегировании страниц была введена схема портлетов.

Затем на сцену ворвался Ajax и родилась система Web 2.0. Ajax позволил программировать асинхронные запросы. Полезная информация больше не нуждалась в разметке, а могла быть представлена в формате XML, а позднее и JSON. Контроллер переместился из серверной части в клиентскую, а задачу агрегирования страниц переложили на все более мощные и стандартизированные клиентские браузеры. Итак, на одном конце спектра есть Web-приложение, где браузер мало отличается от простого визуализатора разметки, и всеми функциями и потоками управляет сервер. А на другом ― такое Web-приложение, в котором большая часть логики управления и потоков выполняется в клиентском браузере (на базе JavaScript, Flash или Silverlight), а сервер обеспечивает службы RESTful для предоставления данных и выполнения любых вычислений, которые могут потребоваться приложению.

Когда нужны мобильные Web-приложения, на ум обычно приходит именно этот последний шаблон, потому что именно так работают мобильные схемы вроде jQuery Mobile или Dojo Mobile. Но что происходит, когда приложение, которое нужно сделать мобильным, имеет дизайн, больше ориентированный на сервер, лишенный REST-служб?


Web-приложение как черный ящик

Поскольку цель заключается в том, чтобы избежать любых изменений кода приложения, его можно рассматривать как черный ящик и сосредоточиться только на том, что получается на выходе при заданном состоянии входа; под входом здесь понимаются HTTP-запросы, а под выходом ― к HTTP-ответы. Таким образом, при создании мобильного клиента, имитирующего определенный HTTP-запрос и способного анализировать и выявлять все возможные ответы на этот запрос, появляется возможность создать мобильную "обложку", которая использует те же модели запрос/ответ, что и существующее приложение. В теории это выглядит достаточно просто, но на практике все может оказаться сложнее.

Последовательность входа в систему

Рассмотрим пример последовательности проверки подлинности пользователя типичного Web-приложения. (Сведения о HTTP-запросах и ответах можно собрать с помощью такого инструмента, как Firefox Firebug или Developer Tools в Chrome или Safari.) Начнем аутентификацию с момента передачи учетных данных. В листинге 1 показана исходная разметка формы входа на сайт.

Листинг 1
<form name="logonForm" action="/abc/salesportal/j_security_check" method="post">
<input type="hidden" name="page" value="signin.wss" />
<table cellspacing="0" cellpadding="0" class="sign-in-table">
   <tr>
   <td class="c1"><label for="user_id">Email ID:</label></td>
   <td><input dir="ltr" class="wpsEditField" size="30" 
         value="" name="j_username" id="user_id" type="text" /> 
	   (e.g., joe@abc.com)</td>
   </tr>
   <tr>
   <td class="c1"><label for="passwd">Password:</label></td>
   <td><input dir="ltr" class="wpsEditField" size="30" id="passwd" 
	   name="j_password" type="password" /></td>
   </tr>
   <tr><td><input type="hidden" name="login-form-type" value="pwd" /></td> </tr>
	   <tr>	<td>&nbsp;</td>
   <td><p><a href="http://www.abc.com/password">Forgot your password?
	   </a></p></td>
   </tr>
</table>

<table cellspacing="0" cellpadding="0" class="submit-table">
<tr>
   <td class="buttons" align="right">
   <span class="button-blue">
   <input type="submit" value="Submit" name="submitButton" />
   </span>
   <span class="button-blue">
   <input type="button" value="Cancel" name="cancelButton" 
	   onclick="cancelSignIn()" />
   </span>
   </td>
   </tr>
</table>
</form>

В приведенном выше коде, когда пользователь вводит свое имя, пароль и нажимает кнопку "Отправить", учетные данные передаются в j_security_check. Достаточно просто. Давайте сымитируем запрос проверки подлинности.

Очень полезный инструмент для тестирования Web-приложения как черного ящика ― cURL, который позволяет строить и прослеживать HTTP-взаимодействия (см. раздел Ресурсы). Он предоставляет полный контроль, позволяя строить каждый аспект запроса (листинг 2). Кроме того, не нужно беспокоиться о том, что у разных браузеров может быть разный кэш.

Листинг 2
curl --verbose --silent --show-error -d "page=signin.wss" --data-urlencode 
"j_username=-joe@abc.com" --data-urlencode "j_password=my+passw0rd" --data-urlencode 
"submit=Submit" http://www.abc.com/abc/salesportal/j_security_check

Обратите внимание, что в этой команде cURL все поля ввода пользователя имеют URL, закодированный с помощью --data-urlencode. Она также автоматически присваивает application/x-www-form-urlencoded заголовок content-type. Кроме того, использование флага --data* означает, что это будет запрос POST. В cURL существует много флагов, и можно воссоздать любой необходимый запрос.

К сожалению, как видно из листинга 3, эта простая форма POST не дает ожидаемого результата.

Листинг 3
* About to connect() to www.abc.com port 80 (#0)
*   Trying 192.168.1.1... connected
* Connected to www.abc.com (192.168.1.1) port 80 (#0)
> POST /abc/salesportal/j_security_check HTTP/1.1
> User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5
> Host: www.abc.com
> Accept: */*
> Content-Length: 91
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 302 Found
< Date: Fri, 18 Nov 2011 16:57:39 GMT
< Server: IBM_HTTP_Server/6.1.0.27 Apache/2.0.47
< Location: http://www.abc.com/abc/salesportal/
< Expires: Thu, 01 Dec 1994 16:00:00 GMT
< Cache-Control: no-cache="set-cookie, set-cookie2"
< Content-Type: www/unknown
< Content-Language: en-US
< Content-Length: 0
< Proxy-Connection: Keep-Alive
< Connection: Keep-Alive
< Set-Cookie: JSESSIONID=0001KBumJR-0VZ6ybqAm1WIOn-N:15lgjrufm; Path=/
< Set-Cookie: LtpaToken2=g2VF5mHGQCmv73URFPz...zmsD5ifgdfSiJghQA/jJdUoABkaik=; 
	Path=/; Domain=.abc.com
< Set-Cookie: LtpaToken=3UpRmGDYVqd+1ZdKm03v...DSyZhJz4wpt+BFeBXTGn0Ww==; Path=/; 
	Domain=.abc.com
<
* Connection #0 to host www.abc.com left intact
* Closing connection #0

Присутствие куки-файлов LTPA указывает на то, что проверка подлинности, похоже, выполнена, но запрос перенаправлен по адресу /abc/salesportal/.

Если добавить в cURL-запрос флаг --location, то перенаправление будет автоматически отслеживаться. При следующей попытке в листинге 4 мы видим первоначальный POST и редирект 302 (как и выше), а также последующие запрос и ответ.

Листинг 4
* Connection #0 to host www.abc.com left intact
* Issue another request to this URL: 'http://www.abc.com/abc/salesportal/'
* Violate RFC 2616/10.3.3 and switch from POST to GET
* Re-using existing connection! (#0) with host www.abc.com
* Connected to www.abc.com (192.168.1.1) port 80 (#0)
> GET /abc/salesportal/ HTTP/1.1
> User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5
> Host: www.abc.com
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 18 Nov 2011 16:39:04 GMT
< Server: IBM_HTTP_Server/7.0.0.15
< Cache-Control: max-age=300
< Expires: Fri, 18 Nov 2011 16:44:04 GMT
< Content-Type: text/html;charset=ISO-8859-1
< Content-Length: 820
< Proxy-Connection: Keep-Alive
< Connection: Keep-Alive
< Age: 1545
<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
 <head>
  <title>Index of /abc/salesportal</title>
 </head>
 <body>
<h1>Index of /abc/salesportal</h1>
<ul><li><a href="/abc/"> Parent Directory</a></li>
<li><a href="ccr/"> ccr/</a></li>
<li><a href="downloads/"> downloads/</a></li>
<li><a href="home2/"> home2/</a></li>
<li><a href="html/"> html/</a></li>
<li><a href="images/"> images/</a></li>
<li><a href="js/"> js/</a></li>
<li><a href="media/"> media/</a></li>
<li><a href="regs/"> regs/</a></li>
<li><a href="style/"> style/</a></li>
</ul>
</body></html>
* Connection #0 to host www.abc.com left intact
* Closing connection #0

Обратите внимание, что хотя и кажется, что проверка подлинности прошла успешно, результирующая страница ― это всего лишь список каталогов. Это не то, чего мы ожидали. Рассмотрим поток аутентификации подробнее. Для этого откроем Chrome и воспользуемся его инструментарием Developer Tools для просмотра деталей сетевых соединений в процессе успешного входа (рисунок 1).

Рисунок 1. Chrome Developer Tools
Рисунок 1. Chrome Developer Tools

В листинге 5 можно заметить, что запрос передает полномочия, возвращает некоторые куки, и затем выполняет перенаправление. Присмотритесь к заголовкам запроса. Передаются не только данные формы (которые могут иметь скрытые поля), но и куки.

Куки AbcSurvey и UnicaNOIDID выглядят безобидными, это просто обследование и анализ сайта. Куки JSESSIONID относится к сеансу на стороне сервера (Java ™), который, вероятно, не содержит ничего ценного, чего нельзя легко воссоздать, так как мы только что прибыли на Web-сайт и фактически ничего не сделали. Однако есть еще один, особенный куки-файл: WASReqURL.

Листинг 5
Request URL: https://www.abc.com/abc/salesportal/j_security_check
Request Method:POST
Status Code:302 Found
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Cache-Control:max-age=0
Connection:keep-alive
Content-Length:105
Content-Type:application/x-www-form-urlencoded
Cookie:
abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; 
JSESSIONID=0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945; 
WASReqURL=https:///abc/salesportal/signin.wss; 
Host:www.abc.com
Origin: https://www.abc.com
Referer: https://www.abc.com/abc/salesportal/registration/signIn.jsp
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 +
(KHTML, like Gecko) Chrome/15.0.874.106 Safari/535.2
Form Data
page:signin.wss
j_username:joe@abc.com
j_password: my+passw0rd
login-form-type:pwd
submitButton:Submit
Response Headers
Cache-Control:no-cache="set-cookie, set-cookie2"
Connection:Keep-Alive
Content-Language:en-US
Content-Length:0
Content-Type:www/unknown
Date:Thu, 17 Nov 2011 19:08:17 GMT
Expires:Thu, 01 Dec 1994 16:00:00 GMT
Location:https://www.abc.com/abc/salesportal/signin.wss
Server:IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47
Set-Cookie:
WASReqURL=""; Expires=Thu, 01 Dec 1994 16:00:00 GMT; Path=/
LtpaToken2=jxSSeGottXC2WQZhQFeXf...PP1u+D/8PFtGPQs0sjiYanjdBJEfF0Zj6kXt1QeE=; 
Path=/
LtpaToken=bTzeMj+SSZFQMUKSZ20tc1...qPSY0kR5MFrKrqZDOwd7twfpupFNUpZUKlEGE41c=; 
Path=/

Похоже, что на адрес перенаправления влияет именно присутствие WASReqURL.

Затем ответ удаляет WASReqURL, делая истекшим срок его действия. Еще он отправляет два новых куки-файла: LtpaToken и LtpaToken2, которые возвращаются только после успешного входа в систему. URL-адрес перенаправления содержится в заголовке Location. Далее следует запрос, приведенный в листинге 6. Обратите внимание, что в запрос направляются новоиспеченные куки LTPA, так как WASReqURL, фактически, удален. Теперь Web-приложение рассматривает этот запрос к /signin.wss как аутентифицированный запрос. В ответ на этот второй запрос возвращаются два новых куки-файла: ODC_CG и ODC_REG.

Листинг 6
Request URL:https://www.abc.com/abc/salesportal/signin.wss
Request Method:GET
Status Code:200 OK
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Cache-Control:max-age=0
Connection:keep-alive
Cookie:
abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; JSESSIONID=
0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945; 
LtpaToken2=jxSSeGottXC2WQZh...anjdBJEfF0Zj6kXt1QeE=; 
LtpaToken=bTzeMj+SSZFQMUKSZ...twfpupFNUpZUKlEGE41c=
Host:www.abc.com
Referer:https://www.abc.com/abc/salesportal/registration/signIn.jsp
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 
(KHTML, like Gecko) Chrome/15.0.874.106 Safari/535.2

Response Headers
Cache-Control:no-cache="set-cookie, set-cookie2"
Connection:Keep-Alive
Content-Encoding:gzip
Content-Language:en-US
Content-Type:text/html;charset=ISO-8859-1
Date:Thu, 17 Nov 2011 19:08:20 GMT
Expires:Thu, 01 Dec 1994 16:00:00 GMT
Server:IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47
Set-Cookie:
ODC_CG=123456; Domain=abc.com
ODC_REG=Joe Park|joe@abc.com; Expires=Fri, 16 Nov 2012 19:08:19 GMT; 
Path=/; Domain=abc.com
Transfer-Encoding:chunked

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

Тестирование последовательности

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

Листинг 7
curl --verbose --silent --show-error -d "page=signin.wss" --data-urlencode 
"j_username=-joe@abc.com" --data-urlencode "j_password=my+passw0rd" --data-urlencode 
"submit=Submit" --cookie "WASReqURL=http:///abc/salesportal/signin.wss" --cookie-jar 
login-cookies.txt http://www.abc.com/abc/salesportal/j_security_check

Без сохранения куки cURL не ведет себя должным образом. Получив заголовок Set-Cookie для WASReqURL, указывающий, что срок куки-файла истек, cURL игнорирует это и в последующем запросе перенаправления продолжает пересылать cookie (без изменений). Чтобы исправить это, включите режим сохранения куки с помощью --cookie jar. Теперь любые изменения куки-файла в редиректе 302 передаются в следующий запрос.

Обратите внимание, что к запросу добавлен куки-файл WASReqURL. Необходимо также иметь возможность записывать куки cURL (--cookie jar, см. врезку), так как в процессе перенаправления срок действия WASReqURL истекает.

Выход cURL показан в листинге 8.

Листинг 8
* About to connect() to www.abc.com port 80 (#0)
*   Trying 192.168.1.1... connected
* Connected to www.abc.com (192.168.1.1) port 80 (#0)
> POST /abc/salesportal/j_security_check HTTP/1.1
> User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5
> Host: www.abc.com
> Cookie: WASReqURL=http:///abc/salesportal/signin.wss
> Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
> Content-Length: 91
> Content-Type: application/x-www-form-urlencoded

POST-запрос производится с помощью куки WASReqURL (листинг 9).

Листинг 9
< HTTP/1.1 302 Found
< Date: Fri, 18 Nov 2011 19:40:56 GMT
< Server: IBM_HTTP_Server/6.1.0.27 Apache/2.0.47
< Location: http://www.abc.com/abc/salesportal/signin.wss
< Expires: Thu, 01 Dec 1994 16:00:00 GMT
< Cache-Control: no-cache="set-cookie, set-cookie2"
< Content-Type: www/unknown
< Content-Language: en-US
< Content-Length: 0
< Proxy-Connection: Keep-Alive
< Connection: Keep-Alive
* Added cookie JSESSIONID="0001iuuXHCo5zHq5oTRRZLDYy4B:15lgjrufm" for domain 
	www.abc.com, path /, expire 0
< Set-Cookie: JSESSIONID=0001iuuXHCo5zHq5oTRRZLDYy4B:15lgjrufm; Path=/
* Added cookie WASReqURL="""" for domain www.abc.com, path /, expire 786297600
< Set-Cookie: WASReqURL=""; Expires=Thu, 01 Dec 1994 16:00:00 GMT; Path=/
* Added cookie LtpaToken2="gsHvV95vwKOU8D1CBzy5u1...z6WI0RUTpn9RJLc6OVc4LlFvh2UnanTo0=" 
	for domain abc.com, path /, expire 0
< Set-Cookie: LtpaToken2=gsHvV95vwKOU8D1CBzy5u1...z6WI0RUTpn9RJLc6OVc4LlFvh2UnanTo0=; 
	Path=/; Domain=.abc.com
* Added cookie LtpaToken="3UpRmGDYVqd+1Zd788+5x7xr8Km03vwUqWkw4...wpt+BFSht7DYg/
	deBXTGn0Ww==" for domain abc.com, path /, expire 0
< Set-Cookie: LtpaToken=3UpRmGDYVqd+1Zd788+5x7xr8Km03vwUqWkw4...wpt+BFSht7DYg
	/deBXTGn0Ww==; Path=/; Domain=.abc.com

Мы получаем редирект 302 по адресу /signin.wss. Срок действия куки WASReqURL истек, и так как учетные данные действительны, возвращаются два куки LTPA (листинг 10).

Листинг 10
* Connection #0 to host www.abc.com left intact
* Issue another request to this URL: 'http://www.abc.com/abc/salesportal/signin.wss'
* Violate RFC 2616/10.3.3 and switch from POST to GET
* Re-using existing connection! (#0) with host www.abc.com
* Connected to www.abc.com (192.168.1.1) port 80 (#0)
> GET /abc/salesportal/signin.wss HTTP/1.1
> User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5
> Host: www.abc.com
> Cookie: LtpaToken2=gsHvV95vwKOU8D1CBzy5u1...z6WI0RUTpn9RJLc6OVc4LlFvh2UnanTo0=; 
JSESSIONID=0001iuuXHCo5zHq5oTRRZLDYy4B:15lgjrufm; LtpaToken=3UpRmGDYVqd+1Zd788+
5x7xr8Km03vwUqWkw4...wpt+BFSht7DYg/deBXTGn0Ww==; WASReqURL=http:///abc/salesportal/
signin.wss
> Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Происходит перенаправление по указанному URL (/signin.wss), и передаются куки LTPA и JSESSIONID. Заметим, что cURL ошибочно указывает, что куки-файл WASReqURL переслался, несмотря на то, что срок его действия, согласно предыдущему ответу, уже истек. Однако так как мы разрешили сохранять куки в cURL (--cookie jar), куки-файл все же не был отправлен.

Содержимое файла cookie-jar показано в листинге 11.

Листинг 11
# Netscape HTTP Cookie File
# http://curl.haxx.se/rfc/cookie_spec.html
# This file was generated by libcurl! Edit at your own risk.

www.abc.com FALSE /     FALSE  0          JSESSIONID  0001iuu...RRZLDYy4B:15lgjrufm
www.abc.com FALSE /     FALSE  786297600  WASReqURL   ""
.abc.com    TRUE  /     FALSE  0          LtpaToken2  gsHvV8D1CBzy5...I0RUTpn2UnanTo0=
.abc.com    TRUE  /     FALSE  0          LtpaToken   3s1Zd88+5x...hJz4wpt+BXTGn0Ww==
.abc.com    TRUE  /abc/salesportal/ FALSE	0  ODC_CG   ""
.abc.com    TRUE  /     FALSE  1353181257 ODC_REG     Joe Smith|joe@abc.com

Запрос GET по адресу /signin.wss завершается успешно. Он возвращает еще два куки: ODC_CG и ODC_REG (листинг 12).

Листинг 12
> HTTP/1.1 200 OK
> Date: Fri, 18 Nov 2011 19:40:57 GMT
> Server: IBM_HTTP_Server/6.1.0.27 Apache/2.0.47
> Expires: Thu, 01 Dec 1994 16:00:00 GMT
> Cache-Control: no-cache="set-cookie, set-cookie2"
> Content-Type: text/html;charset=ISO-8859-1
> Content-Language: en-US
> Transfer-Encoding: chunked
> Proxy-Connection: Keep-Alive
> Connection: Keep-Alive
* Added cookie ODC_CG="""" for domain abc.com, path /abc/salesportal/, expire 0
> Set-Cookie: ODC_CG=""; Domain=abc.com
* Added cookie ODC_REG="Joe Smith|joe@abc.com" for domain abc.com, path /, expire 
1353181257
> Set-Cookie: ODC_REG=Joe Smith|joe@abc.com; Expires=Sat, 17 Nov 2012 19:40:57 
GMT; Path=/; Domain=abc.com
>
* Connection #0 to host www.abc.com left intact
* Closing connection #0

Разметка, возвращенная в этом последнем ответе (не показана), соответствует домашней странице после аутентификации, так что все входные данные (как и перенаправление истекших куки) привели к успешному входу.

Резюме по последовательности входа

Во всей этой последовательности можно выделить входные сообщения аутентификации (таблица 1) и ответы после успешного входа (таблица 2).

Таблица 1
Тип входного сообщенияСодержание
КукиabcSurvey=1321469743354; <<< UNIMPORTANT
UnicaNIODID=h8DDq7LCdW1-XQbeXFj; <<< UNIMPORTANT
JSESSIONID=0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945; <<< UNIMPORTANT
WASReqURL=https:///abc/salesportal/signin.wss;
Формаpage:signin.wss <<< HIDDEN
j_username:joe@abc.com
j_password: my+passw0rd
login-form-type:pwd <<< HIDDEN
submitButton:Submit <<< UNIMPORTANT
Данные формы кодируются какContent-Type:application/x-www-form-urlencoded
Таблица 2
Тип выходного сообщения Содержание
КукиWASReqURL=https:///abc/salesportal/signin.wss; <<< CREATED then DESTROYED
LtpaToken2=jxSSeGottXC2WQZhQFeXfGHapYGLTzXDDvLf...cl8PFtGPQs0sjiYanjdBJEfF0Zj6kXt1QeE=; Path=/
LtpaToken=bTzeMj+SSZFQMUKSZ20tc1efTrTZodEkEkqPt...nR5MFrKrqZDOwd7twfpupFNUpZUKlEGE41c=; Path=/
ODC_CG=123456; Domain=abc.com
ODC_REG=Joe Smith|joe@abc.com; Expires=Fri, 16 Nov 2012 19:08:19 GMT; Path=/; Domain=abc.com

Разметка домашней страницы после аутентификации показана в листинге 13.

Листинг 13
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en-US">
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
<meta name="description" content="Registered Home Page Salesportal" />
<meta name="keywords" scheme="iso8601" 
content="ABC, Salesportal, sellers, home, registered, signed in" /> ...

Таким образом, чтобы создать страницу входа для мобильных устройств, нужно иметь возможность отправлять те же сообщения. Сообщения типа формы ― не проблема; проблемы создают куки, а именно, WASReqURL.

Страница входа для мобильных устройств станет частью нового статического файла ― "мобильной обложки", которая упоминалась выше. Назовем его mobile.html. Он будет обслуживаться с того же сервера, что и существующее приложение, поэтому не помешает политика безопасности Single Origin security Policy (SOP). Когда пользователь отправляет запрос на проверку подлинности с мобильного устройства, он передается посредством XMLHttpRequest (XHR) который, как известно, повторяет запрос HTTP, но выполняется за кулисами. Однако эти запросы XHR или Ajax не идентичны обычному запросу HTTP. В программном XHR-запросе нельзя просто задать заголовок Cookie (как мы делали с помощью утилиты cURL). Согласно спецификации W3 для XMLHttpRequest, этот конкретный заголовок запрещен.

Кроме того, создание куки WASReqURL на стороне клиента не приведет к его включению в следующий ответ серверу, потому что домен cookie может быть задан только на стороне сервера. Единственный способ включить этот куки в запрос ― сначала получить его от сервера. Для этого нужно знать, когда был создан куки WASReqURL. Запрос страницы входа показан в листинге 14.

Листинг 14
Request URL:https://www.abc.com/abc/salesportal/signin.wss
Request Method:GET
Status Code: 302 Found
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Connection:keep-alive
Cookie: abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; JSESSIONID=
0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945
Host: www.abc.com
Referer: https://www.abc.com/abc/salesportal/home.wss
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, 
like Gecko) Chrome/15.0.874.106 Safari/535.2

Response Headers
Cache-Control: no-cache="set-cookie, set-cookie2"
Connection: Keep-Alive
Content-Language: en-US
Content-Length: 0
Content-Type: www/unknown
Date: Thu, 17 Nov 2011 18:50:02 GMT
Expires: Thu, 01 Dec 1994 16:00:00 GMT
Location: https://www.abc.com/abc/salesportal/registration/signIn.jsp
Server: IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47
Set-Cookie: WASReqURL=https:///abc/salesportal/signin.wss; Path=/

Обратите внимание, что этот первоначальный запрос страницы signin.wss перенаправляется на /registration/signIn.jsp. Еще важнее то, что в ответе возвращается куки WASReqURL. Этот куки отправляется во втором запросе (листинг 15). (Ошибка в Safari 5.1.1 для Windows препятствует перенаправлению куки, созданных в ответе 302, в последующих запросах GET. Но в Safari для iOS это работает.)

Листинг 15
Request URL:https://www.abc.com/abc/salesportal/registration/signIn.jsp
Request Method:GET
Status Code:200 OK
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Connection:keep-alive
Cookie:abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; JSESSIONID=
0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945; WASReqURL=https:///abc/salesportal/
signin.wss
Host:www.abc.com
Referer:https://www.abc.com/abc/salesportal/home.wss
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, 
like Gecko) Chrome/15.0.874.106 Safari/535.2
Response Headers
Connection:Keep-Alive
Content-Encoding:gzip
Content-Language:en-US
Content-Type:text/html;charset=ISO-8859-1
Date:Thu, 17 Nov 2011 18:50:03 GMT
Server:IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47
Transfer-Encoding:chunked

Воссоздание последовательности входа с помощью Ajax

В ходе этого расследования мы выяснили, что мобильный клиент должен выполнить запрос GET по адресу /signin.wss с единственной целью получения куки WASReqURL перед отправкой формы для входа.

При использовании jQuery этот запрос будет выглядеть как в листинге 16.

Листинг 16
$.ajax({
	type: 'GET',
	url: '/abc/salesportal/signin.wss',	
	async: false,
	complete: function(xhr, textStatus) {
		 if(xhr.readyState == 4) 
		 {
			 if(xhr.status == 200) { console.log(document.cookie); }
		 }
	}
});

В консоли JavaScript вы должны увидеть WASReqURL=http:///abc/salesportal/signin.wss (среди других куки).

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

Как только пользователь представил свои учетные данные, в захваченную последовательность включается POST по адресу /abc/salesportal/j_security_check, что приводит к перенаправлению 302 по адресу /abc/salesportal/signin.wss. HTTP также перенаправляет результат в другой метод, в данном случае, из POST в GET. Любые куки, возвращенные в первом запросе, будут перенаправлены во второй запрос (если это разрешено в Path для куки). Все это происходит прозрачно в запросе Ajax. Программно ни редирект 302, ни промежуточный ответ и (повторный) запрос увидеть нельзя (листинг 17).

Листинг 17
var params = 'j_username=' + encodeURIComponent($('#user_id').val()) + '&j_password=' 
+ encodeURIComponent($('#passwd').val()) + '&page=signin.wss&submit=Submit';

$.ajax({
	type: 'POST',
	url:'/abc/salesportal/j_security_check',	
	contentType: 'application/x-www-form-urlencoded',
	data: params,				
	cache: false,
// Нельзя задать заголовок Cookie - запрещено		
//	headers:{'Cookie':'WASReqURL=http:///abc/salesportal/signin.wss'},
// Еще одна попытка задать заголовок Cookie:
//	beforeSend: function(xhr){ xhr.setRequestHeader( "Cookie", 
"WASReqURL=http:///abc/salesportal/signin.wss"); },
		complete: function(xhr, textStatus) {  …}
});

Не забудьте зашифровать информацию, вводимую пользователем в форму, с помощью encodeURIComponent(), когда данные типа application/x-www-form-urlencoded передаются в запросе POST.

Отправив запрос аутентификации, необходимо как-то узнать, что проверка прошла успешно.

Проблема в том, что в обоих случаях запрос будет (прозрачно) перенаправлен на новую страницу. При подходе RESTful вы просто получили бы код ответа 2xx в случае успеха или 401 при попытке несанкционированного доступа. Или же вы могли бы получить специальный ответ, упакованный в полезные данные JSON. Однако для данного конкретного Web-приложения эти удобные формы ответа недоступны, поэтому нам придется работать с текущей моделью "запрос-ответ":

  • успешный вход будет перенаправлен на адрес /abc/salesportal/signin.wss;
  • неудачная попытка входа будет перенаправлена на адрес /abc/salesportal/registration/signInError.jsp;
  • Окончательный код состояния HTTP в обоих случаях будет 200 (ОК).

Прозрачное перенаправление запросов Ajax проблематично, потому что клиент не получит никакого уведомления о переадресации или конечного URL (заголовок Location редиректа 302 теряется).

Объект JavaScript XHR не предоставляет доступ к запросу, а это означает, что конечный URL запроса неизвестен. Все, что доступно для определения результата обработки запроса, ― это анализ заголовков или полезных данных ответа (на последний запрос).

В листинге 17 это делается в функции complete().

Для упрощения синтаксического анализа разметки ответа преобразуем ответ в формат XML (если он еще не в нем), как показано в листинге 18. (Если нужно обслуживать не только браузеры на основе WebKit, то более надежный способ сделать это приведен в разделе Ресурсы.)

Листинг 18
complete: function(xhr, textStatus) {
if(xhr.readyState == 4) 
{
	if(xhr.status == 200)
	{
		var xmlResponse = xhr.responseXML;
		if(xhr.responseXML == null)
      		 {
			if(typeof DOMParser != "undefined") 
			{ 	                     
				var parser = new DOMParser();
				xmlResponse = parser.parseFromString(xhr.responseText,
				"text/xml");
			}
		}	
	}
	/*
	  else if(xhr.status == 302 || xhr.status == 303) {} // этого никогда не будет,, 
только 200 для страницы signInError
	  else if(xhr.status == 401) // UNAUTHORIZED - этого никогда не будет, 
так как приложение перенаправляет запрос на страницу signInError
	*/
}}

После разбора ответа DOM следующий шаг заключается в однозначной идентификации данных со страниц signin.wss и signInError.jsp, уникальных для этих страниц. Они должны присутствовать всегда, и в идеале это должно быть что-то, что не изменится в случае преобразования страницы при изменении пользовательского интерфейса. Прямые кандидаты на эту роль - метатеги. Здесь используются теги ключевых слов, потому что страница успешного входа содержала ключевые слова signed in, а страница ошибки ― failure и error (листинг 19).

Листинг 19
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/
xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en-US">
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
<meta name="description" content="Registered Home Page Salesportal" />
<meta name="keywords" scheme="iso8601" content="ABC, Salesportal, sellers, home, 
registered, signed in" /> ...

При использовании ответа DOM, полученного выше, весьма полезным для извлечения содержимого из метатега ключевых слов оказываются селекторы jQuery (листинг 20).

Листинг 20
var isErrorPage = true;
var metaKeywordsTag = $('meta[name="keywords"]',xmlResponse);

if(metaKeywordsTag != null)
{
	var keywords = metaKeywordsTag.attr("content");

	if(keywords != null && ((keywords.indexOf("fail") >= 0) || (keywords.indexOf
		("error") >= 0)))
	{ isErrorPage = true; }
	else
	{
		if(keywords != null && (keywords.indexOf("signed in") >= 0))
			isErrorPage = false;
	}                		                		
}

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


Заключение

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

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

Ресурсы

Научиться

Получить продукты и технологии

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



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

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=WebSphere, Мобильные приложения
ArticleID=877418
ArticleTitle=Адаптация Web-приложений к мобильным устройствам без изменения кода
publish-date=04232013