在不更改应用程序代码的情况下对 Web 应用程序启用移动功能

许多现有的网站在设计时都没有考虑到移动设备。要为移动网络重新构建它,可能需要花费大量的投资。本文将演示如何在不更改任何服务器端代码的情况下为现有 Web 应用程序提供一个移动接口。 本文来自于 IBM WebSphere Developer Technical Journal 中文版

Christian Voth, 认证的 IT 架构师, IBM

Christian Voth 是 IBM 软件服务部的一名认证的 IT 架构师,精通 Web 技术和企业 Java。他拥有多伦多大学计算机科学专业学位,目前效力于位于加拿大的 IBM Toronto Lab。



2013 年 9 月 16 日

简介

尽管随着 Android、iOS 和其他移动操作系统的版本不断升级,移动浏览器变得越来越强大,但物理局限仍然存在,比如较小的屏幕和使用触摸作为新输入模式(而不是使用鼠标)。尽管一些智能电话或许可以显示一个网站,但最终的显示结果常常难以使用,甚至完全无用。按钮和其他控件需要调整大小,以便能够通过手指而不是鼠标指针轻松操作它们,而且滑动和多点触摸手势等新动作能被高效地利用。简言之,需要各方齐心协力,才能使这些站点不仅仅是移动友好的,而是支持移动的。

减少的屏幕可用空间和触摸控制是促进简化和重组 Web UI,使 Web 应用程序适用于移动设备的主要因素。尽管可以生成在桌面和移动浏览器上以不同方式呈现的 单一 UI,但此方法通常用于避免对现有 Web 应用程序的任何修改。相反,目标应该是生成一组 静态资产(标记、样式表和 JavaScript™代码),它们提供了对 Web 应用程序功能的移动访问,无需以任何方式修改服务器端代码。现有的站点仍然原封不动。

从操作角度讲,这避免了将代码更改部署到生产环境中的高昂成本。它还消除了对在添加移动支持期间修改的现有功能进行回归测试的需求。另外,因为使用了相同的请求和响应,所以如果期望添加移动支持来显著提升您的访问流量,那么只需对应用程序执行性能测试即可。借助此技术,可简单地插入静态文件(.html、.css 和 .js),以便让托管现有 Web 应用程序的同一个域为它们提供服务,重用应用程序已提供的现有服务。

通过将限制自己仅创建新的静态资产,您实际上仅为现有的 Web 应用程序编写了一个客户端。

Web 应用程序:简史

大多数非平凡的 Web 应用程序都是围绕 模型 - 视图 - 控制器(MVC) 的概念构建的,其中控制器位于服务器上。服务器维护会话状态,通过转发或重定向,将用户请求导航到下一个要呈现的页面。每个客户端请求通常会得到整个页面的标记,即使在不同视图中仅更改了该页面的一部分。随后引入了 portlet 框架来解决这个问题,该框架支持更灵活地执行页面聚合。

然后,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 或者 Chrome 或 Safari 中的 Developer Tools。)我们首先看看来自提交凭据的位置的身份验证。清单 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>

在上面的代码中,用户输入用户 ID 和密码并单击 Submit 时,可以看到凭据将传送到 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 命令可确保所有用户输入字段都使用 --data-urlencode进行了 URL 编码。这还会自动将 content-type 标头设置为 application/x-www-form-urlencoded。另外,使用 --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 cookie,您似乎已通过验证,但您的请求已重定向到 /abc/salesportal/

通过将 --location标志添加到您的 cURL 请求中,重定向会自动被跟踪。在下一次尝试时,您会看到最初的 POST 和 302 重定向(同上),以及来自清单 4 中的重定向的后续请求和响应。

清单 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 中的网络详细信息,可以看到请求提交了凭据,返回了一些 cookie,然后是一些重定向。如果忽视了请求标头,您将面临极大的危险。不仅表单数据会被提交(可能还有隐藏的字段),cookie 也会被提交。

abcSurveyUnicaNOIDIDcookie 看起来是无害的,只是一个调查和站点分析。JSESSIONIDcookie 指一个服务器端会话 (Java™),它可能不包含任何无法轻松重建的有价值的东西,因为您已经到达网站,并且还未实际做任何事情。但是,有一个 cookie 非常引人注目: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 cookie 的存在影响了重定向位置。

响应随后会让 WASReqURL cookie 过期,以便删除它。它还会发送两个新的 cookie:LtpaTokenLtpaToken2,二者都仅在成功登录后才返回。Location 标头包含作为重定向目标的 URL。下一个请求如清单 6 所示。请注意,它将转发请求中新创建的 LTPA cookie,因为 WASReqURL 已被实际删除。Web 应用程序现在会将这个 /signin.wss请求视为经过验证的请求。第二个请求返回两个新的 cookies:ODC_CGODC_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 的 Developer Tools 中的网络跟踪功能所提供的洞察,您可以创建一个新的 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.txthttp://www.abc.com/abc/salesportal/j_security_check

如果不保存 cookie,cURL 没有正确的行为。它会收到 WASReqURL 的 Set-Cookie 标头,指示该 cookie 已过期后,cURL 似乎会忽略该事实,继续在随后重定向的请求中(原封不动地)转发该 cookie。要修复此问题,可以使用 --cookie-jar 启用 cookie 持久性。启用之后,一个 302 中的任何 cookie 更改都会传递给下一个请求。

请注意,WASReqURL cookie 已添加到请求中。也有必要让 cURL 记录 cookie(--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 cookie 发出(清单 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

您获得了一个向 /signin.wss的 302 重定向。WASReqURL cookie 已过期,而且因为您的凭据是正确的,所以您会获得两个 LTPA cookie(清单 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) 被关注,LTPAand JSESSIONIDcookie 也被转发。这里请注意,cURL 错误地表明 WASReqURL cookie 已被转发,而事实是它已在前一个响应中过期。但是,因为您已在 cURL 中启用了 cookie 持久性 (--cookie-jar),所以,无论 cURL 表明了什么,都没有发送该 cookie。

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

针对 /signin.wss 的 GET已成功完成。它返回了另外两个 cookie:ODC_CGODC_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

最终的响应(未显示)中返回的标记针对的是经过验证的主页,所以您知道所有输入(以及过期 cookie 的正确转发)都得到了一次成功的身份验证。

登录序列总结

从这个完整的序列中,您已确定了身份验证输入(表 1)以及在成功登录后的输出(表 2)。

表 1
输入类型详细信息
CookieabcSurvey=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
输出类型详细信息
CookieWASReqURL=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" /> ...

所以,要创建一个移动登录页面,需要能够发送相同的输入。表单输入方面没有问题,但 cookie(比如 WASReqURL)会比较棘手。

您的移动登录页面将是您创建的一个新的静态文件(之前提到的移动 “皮肤”)的一部分。我们将它称为 mobile.html。它将由与现有应用程序相同的服务器提供服务,所以 Single Origin security Policy (SOP) 不会是一个阻碍。当用户提交一个移动身份验证请求时,它将由 XMLHttpRequest(XHR)(众所周知这完全就是一个 HTTP 请求)发送,但在幕后完成。但是,这些 XHR 或 Ajax 请求与典型的 HTTP 请求并不等同。存在一些限制,比如在程序化的 XHR 请求中设置 “Cookie” 标头的能力(就像使用 cURL 实用程序所做的一样)。依据 W3 XMLHttpRequest 规范,这个特定的标头是受限的。

另外,创建 WASReqURL cookie 客户端不会导致它包含在发送回服务器的下一个响应中,因为 cookie 域只能在服务器端设置。将这个 cookie 包含在请求中的惟一方式是,一开始就从服务器接收它。因此,您需要知道何时创建 WASReqURL cookie。对登录页面的请求如清单 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。更重要的是,您会注意到一个 cookie WASReqURL在响应中被发送回来。这个 cookie 在第二个请求中发送(清单 15)。(针对 Windows 的桌面 Safari 5.1.1 中的一个缺陷会阻止在 302 响应中创建的 cookie 在后续 GET 请求中转发。但这在 Safari for 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 重新创建登录序列

根据这次分析,您的移动客户端将需要对 /signin.wss发出一个 GET请求,这么做的惟一目的是在提交登录表单之前收到 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(以及其他 cookie)。

WASReqURL cookie 出现在客户端上后,您可创建 POST 请求,将登录凭据发送到服务器。

用户提交他们的凭据后,您应在捕获的序列中重新调用对 /abc/salesportal/j_security_checkPOST,这会导致一次向 /abc/salesportal/signin.wss的 302 重定向。HTTP 重定向也会导致一次方法更改,在本例中,方法从 POST 更改为 GET。第一个请求中返回的任何 cookie 都会在第二个请求中转发(假设 cookie 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, 
 // Cannot set the Cookie header - restricted 		
 // 	 headers:{'Cookie':'WASReqURL=http:///abc/salesportal/signin.wss'}, 
 // Another attempt to set the Cookie header: 
 // 	 beforeSend: function(xhr){ xhr.setRequestHeader( "Cookie", 
"WASReqURL=http:///abc/salesportal/signin.wss"); }, 
		 complete: function(xhr, textStatus) {  … } 
 });

请记住,在 POST application/x-www-form-urlencoded内容类型的数据时,应该使用 encodeURIComponent()对用户提供的基于表单的输入进行编码。

在发送您的身份验证请求后,您需要知道验证成功与否。

两种情况下的问题是:请求将被(透明地)重定向到一个新页面。采用 RESTful 方法,您会获得响应代码 2xx,这表示验证成功, 401 表示未授权。您可能会获得一个封装到 JSON 有效负载中的自定义响应代码。但是,对于这个具体的 Web 应用程序,这些便捷的响应不可用,所以我们必须设法使用当前的请求 - 响应模式:

  • 成功的登录将重定向到 /abc/salesportal/signin.wss
  • 不成功的登录将重定向到 /abc/salesportal/registration/signInError.jsp
  • 在两种情况下,最终的 HTTP 状态代码都将是 200(OK)。

Ajax 请求的重定向透明性存在问题,因为客户端不会收到任何重定向通知,不知道最终的 URL 是什么(302 重定向的 Location 标头已丢失)。

XHR JavaScript 对象没有提供该请求,这意味着不知道最终的请求 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) {} // will never see this, 
 only the 200 for the signInError page 
	  else if(xhr.status == 401) // UNAUTHORIZED - will never see this as 
 application redirects to signInError page 
	 */ 
 }}

解析响应 DOM 后,下一步是惟一地识别 signin.wsssignInError.jsp页面上该页面独有的数据。它需要始终存在,而且在理想情况下应在页面经历 UI 翻新时不会发生更改。元标记是首要的候选者。这里使用了 keywords 标记,因为成功的页面包含 “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 标志,您的移动客户端可显示已验证的页面流中的下一个页面或登录错误对话框。


结束语

从本文中描述的登录示例,您可看到能够以编程方式使用 XmlHTTPRequest,重新创建 Web 应用程序预期的相同的请求 - 响应序列。通过将应用程序视为黑盒,忠实地重新创建输入,并使用可靠技术来解释输出,可以避免为提供移动客户端支持而更改现有应用程序。因为没有进行服务器代码更改,所以无需经历部署到生产环境的可能代价高昂的过程,也无需进行广泛的回归和性能测试。

理想情况下,一个网站应在构建之初就考虑到 响应式 Web 设计。但是,现实情况是,许多网站在设计时没有考虑到移动设备,需要进行大量的投资重新构建它们,使它们适用于移动网络。此技术提供了一种折衷方法,让您的网站能够更快地用于移动网络。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=WebSphere, 移动开发
ArticleID=945229
ArticleTitle=在不更改应用程序代码的情况下对 Web 应用程序启用移动功能
publish-date=09162013