深入探索Ajax应用:功能重塑、跨浏览器问题与安全挑战
1. 重塑Web浏览器功能于应用之中
在应用里重塑部分Web浏览器功能是可行的,Google Maps就是绝佳范例。乍看之下,Google Maps和Gmail一样具有可寻址性。访问http://maps.google.com/ 会呈现一幅大比例尺地图,借助Ajax能实现缩放并导航到全球任意地点,然而浏览器地址栏里的URI却始终不变。
不过,Google Maps运用Ajax为当前所在的全球任意地点维护了一个“永久链接”。这个URI并非存于浏览器地址栏,而是在HTML文档的
<a>
标签里。它包含了Google Maps识别全球某区域所需的全部信息:纬度、经度和地图比例尺,是Ajax应用的新入口点,相当于浏览器地址栏。
由于在导航地图时会进行额外的DOM操作来更新这个
<a>
标签,所以地图上的每个点都能在网络上被访问。任何点都能被收藏、在博客中分享以及通过邮件传播。访问这些URI的人能直接进入Google Maps Ajax应用的对应位置,而非像重新加载http://maps.google.com/ 那样看到以美国大陆为中心的视图。Ajax虽破坏了可寻址性,但出色的应用设计又将其找回,这促使像Google Sightseeing(http://google sightseeing.com/)这样围绕Google Maps应用的社区得以发展。
此外,Ajax应用可通过重现浏览器后退和前进按钮的功能来恢复无状态性。不一定要完全照搬浏览器的行为,关键在于让最终用户能在应用状态中前后移动,避免因犯错或迷路而不得不重新开始复杂操作。
2. 跨浏览器问题与Ajax库
涉及Web浏览器时,不同客户端对XMLHttpRequest的支持程度各异,而Internet Explorer是主要的特例。虽说XMLHttpRequest由微软发明,且Internet Explorer是首个支持Ajax的浏览器,但在Internet Explorer 7发布前,Ajax是作为特定于Windows的技术实现的,即名为XMLHttp的ActiveX控件。
跨平台的Mozilla项目采用了XMLHttp控件的API,并将其实现为可直接从JavaScript实例化的类。其他浏览器纷纷效仿,如今所有主流浏览器都使用XMLHttpRequest这个名称(包括新版Internet Explorer)。但旧版Internet Explorer仍占据大量用户群体,所以跨浏览器问题依旧存在。
以下是一个JavaScript函数,它总能创建一个行为类似于XMLHttpRequest的对象,即便其底层可能是ActiveX控件,该函数由Bret Taylor编写,源自他的网站http://ajaxcookbook.org/:
function createXMLHttpRequest() {
if (typeof XMLHttpRequest != "undefined") {
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined") {
return new ActiveXObject("Microsoft.XMLHTTP");
} else {
throw new Error("XMLHttpRequest not supported");
}
}
这个函数可直接替代Example 11 - 3中的XMLHttpRequest构造函数,例如:
// 原代码
request = new XMLHttpRequest();
// 替换后
request = createXMLHttpRequest();
另外还有两个主要的跨浏览器问题:
- Safari浏览器不支持PUT和DELETE方法。若要让服务能从Safari访问,就需允许客户端用重载的POST请求模拟PUT和DELETE请求。
- 微软的Internet Explorer会无限期缓存成功的响应,这会让用户觉得资源未改变,即便实际已变。解决办法是在响应中发送合适的ETag响应头,或使用Cache - Control完全禁用缓存。可通过XMLHttpRequest测试套件(http://www.mnot.net/javascript/xmlhttprequest/)了解更多细微的跨浏览器问题。
由于Ajax对JavaScript应用至关重要,一些JavaScript库包含了用于隐藏浏览器差异的包装器。这里将展示如何使用两个流行的库(Prototype和Dojo)进行简单的HTTP请求。
3. Prototype库
Prototype(http://prototype.conio.net/)引入了三个用于进行HTTP请求的类:
-
Ajax.Request
:是XMLHttpRequest的包装器,能处理跨浏览器问题,并可在请求成功或失败时调用不同的JavaScript函数。实际的XMLHttpRequest对象可作为Request对象的
transport
成员访问,所以
responseXML
可通过
request.transport.responseXML
获取。
-
Ajax.Updater
:是Request的子类,会发起HTTP请求并将响应文档插入到DOM的指定元素中。
-
Ajax.PeriodicalUpdater
:会定期发起相同的HTTP请求,每次刷新DOM元素。
以下是使用Prototype实现的del.icio.us Ajax客户端的部分代码,它主要替换了Example 11 - 3中XMLHttpRequest构造函数所在的代码:
...
<script src="prototype.js"></script>
<script type="text/javascript">
...
var request = new Ajax.Request("https://api.del.icio.us/v1/posts/recent",
{method: 'get', onSuccess: populateLinkList,
onFailure: reportFailure});
function reportFailure() {
setMessage("An error occured: " + request.transport.status);
}
// Called when the HTTP request has completed.
function populateLinkList() {
setMessage("Request complete.");
if (netscape.security.PrivilegeManager.enablePrivilege) {
netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
}
posts = request.transport.responseXML.getElementsByTagName("post");
...
Prototype在简化XMLHttpRequest的过程中隐藏了部分功能,无法设置请求头,也不能指定基本HTTP认证的用户名和密码。所以即便使用Prototype,也可能需要保留类似Example 11 - 7的代码片段。不过,使用Prototype实现的del.icio.us客户端无需用户名和密码文本框,仅需一个按钮,最终用户的浏览器会自动提示输入del.icio.us的用户名和密码。
4. Dojo库
Dojo库(http://dojotoolkit.org/)提供了统一的API,不仅能隐藏不同浏览器在XMLHttpRequest方面的差异,还能隐藏XMLHttpRequest与其他让浏览器发送HTTP请求方式之间的差异。这些“传输方式”包括使用HTML标签的技巧,如JoD。所有XMLHttpRequest的变体都存于
dojo.io.XMLHttp
传输类中。对于所有传输方式,
bind
方法用于发起HTTP请求。
以下是使用Dojo实现的del.icio.us Ajax客户端的部分代码,它主要替换了Example 11 - 3中XMLHttpRequest构造函数所在的代码:
...
<script src="dojo/dojo.js"></script>
<script type="text/javascript">
...
dojo.require("dojo.io.*");
dojo.io.bind({ url: "https://api.del.icio.us/v1/posts/recent", load:
populateLinkList, error: reportFailure });
function reportFailure(type, error) {
setMessage("An error occured: " + error.message);
}
// Called when the HTTP request has completed.
function populateLinkList(type, data, request) {
setMessage("Request complete.");
if (netscape.security.PrivilegeManager.enablePrivilege) {
netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
}
posts = request.responseXML.getElementsByTagName("post");
...
错误处理函数会传入一个
dojo.io.Error
对象,包含
number
和
message
成员。可忽略第一个参数,因为它始终是“error”。成功处理函数的第一个参数也可忽略,它始终是“load”。第二个参数
data
是使用Dojo DOM操作接口的一个接口,若想使用XMLHttpRequest接口,也可忽略该参数。
5. 绕过浏览器安全模型
浏览器通常会执行一条通用规则,防止使用A域名的代码向B域名发起HTTP请求。但这条规则过于严格,下面将介绍两种绕过它的方法:请求代理和JavaScript on Demand(JoD),同时也会说明这些技巧带来的风险。
6. 请求代理
假设运行一个名为example.com的网站,提供的Ajax应用试图向yahoo.com发起XMLHttpRequest请求,客户端的Web浏览器自然会报错。但如果客户端从不直接向yahoo.com发起请求,而是向example.com发起请求,由服务器在不告知客户端的情况下向yahoo.com发起相同请求呢?这就是请求代理技巧,在Yahoo的文档“Use a Web Proxy for Cross - Domain XMLHttpRequest Calls”(http://developer.yahoo.com/javascript/howto - proxy.html)中有详细描述。
在这个技巧中,需在服务器的URI空间中划出一部分来模拟其他服务器的URI空间。当收到该空间内的URI请求时,将其原封不动地转发到外部服务器,再把响应直接传回客户端。从客户端角度看,就像你在提供他人的Web服务,实际上只是替换了HTTP响应中的域名。
若使用Apache且安装了
mod_proxy
,最简单的设置代理的方式是在Apache配置中进行。若还安装了
mod_ssl
,则可启用
SSLProxyEngine
来代理HTTPS请求。即便从HTTP服务器代理HTTPS请求(如将http://example.com/service/ 代理到https://service.com/)也是可行的,但这会破坏连接的安全性,数据在代理和网站之间是安全的,但在网站和最终用户之间不安全,所以这样做时最好告知最终用户。
例如,要让上述的del.icio.us Ajax应用在example.com上运行,可设置代理,使https://example.com/apis/delicious/v1/ 下的所有URI透明地转发到https://api.del.icio.us/v1/。设置代理最简单的方法是使用
ProxyPass
指令:
SSLProxyEngine On
ProxyRequests Off # Don’t act as an open proxy.
ProxyPass /apis/delicious/v1 https://api.del.icio.us/v1/
更灵活的解决方案是使用带有
[P]
标志的重写规则,它能利用正则表达式的全部功能将自己的URI空间映射到外部网站的URI空间:
SSLProxyEngine On
ProxyRequests Off # Don’t act as an open proxy.
RewriteEngine On
RewriteRule ^apis/delicious/v1/(.*)$ https://api.del.icio.us/v1/$1 [P]
通过这样的设置,可从自己的域名提供Ajax应用delicious - ajax.html,而不会触发浏览器安全警告。只需将以下代码:
request.open("GET", "https://api.del.icio.us/v1/posts/recent",
true, username, password);
改为:
request.open("GET", "https://example.com/apis/delicious/v1/posts/recent",
true, username, password);
大多数Apache安装中未安装
mod_proxy
,因为开放的HTTP代理是垃圾邮件发送者和其他不良分子隐藏踪迹的常用工具。若Web服务器没有内置的代理支持,可编写一个小型Web服务作为透明代理并在服务器上运行。例如,代理del.icio.us API请求的Web服务可位于apis/delicious/v1,它会将收到的所有HTTP请求(包括HTTP头)传递到https://api.del.icio.us/v1/ 下的对应URI。Yahoo提供了一个用PHP编写的示例代理服务,硬编码为访问yahoo.com的Web服务(http://developer.yahoo.com/javascript/howto - proxy.html),可参考该示例来创建自己的代理服务。
即便代理配置正确,仅代理Web的一小部分请求时,你和最终用户仍面临风险。为Ajax客户端设置代理时,在用户眼中你要对其他网站的行为负责。代理技巧会让你成为其他网站出现问题时的替罪羊,若Web服务崩溃、欺骗最终用户或滥用其个人数据,看起来就像是你所为。因为在Ajax应用中,最终用户只能看到你的GUI界面,不一定知道浏览器在后台发起HTTP请求,更不知道对你域名的请求被代理到了其他域名。若浏览器知晓此事,会进行干预并阻止。
此外,代理技巧还会让你为客户端的请求负责。客户端可发起任意Web服务请求,看起来就像是你导致的,这可能会让你陷入尴尬或面临法律风险。不过,对于需要单独授权的Web服务,这个问题会相对较小。
7. JavaScript on Demand(JoD)
人类很少主动要求使用JavaScript,但Web浏览器却并不少见。该技巧的基础是HTML的
<script>
标签不一定要包含硬编码的JavaScript代码,它可以有一个
src
属性,引用另一个URI处的代码。当Web浏览器遇到
<script>
标签时,会加载
src
属性中的URI并将其内容作为代码运行。
在之前的JSON示例(Example 11 - 6)中,进行Yahoo!图像搜索查找大象图片时就用到了这一点。
src
属性传统上的用法类似于C语言的
#include
或Ruby的
require
,用于从其他URI加载JavaScript库,如Example 11 - 12所示:
<!-- In a real application, you would save json.js locally
instead of fetching it from json.org every time. -->
<script type="text/javascript" src="http://www.json.org/json.js"></script>
可以看到,
src
属性中的URI不一定要和原始HTML文件在同一服务器上,浏览器安全模型并不认为这不安全,可能是因为在人们认真考虑安全影响之前,
src
属性就已广泛使用。
回顾Example 11 - 6中的大象示例,其中包含如下代码:
<script type="text/javascript"
src="http://api.search.yahoo.com/ImageSearchService/V1/imageSearch
?appid=restbook&query=baby+elephant&output=json&callback=formatImages" />
这个长URI并不像http://www.json.org/json.js 那样指向一个独立的JavaScript库。若在浏览器中访问该URI,会发现其表示的是一段自定义生成的JavaScript代码。在Yahoo的开发者文档(http://developer.yahoo.com/common/json.html)中,Yahoo承诺此类资源的表示是一段JavaScript代码片段,具体来说,是将一个数据结构作为唯一参数传递给URI中指定的回调函数(这里是
formatImages
)的代码片段。生成的JavaScript表示大致如下:
formatImage({"ResultSet":{"totalResultsAvailable":"27170",...}})
当客户端加载HTML页面时,会获取该URI并将其内容作为JavaScript运行,顺便调用
formatImage
方法。这对应用来说很棒,但对Web浏览器而言并非如此。从安全角度看,这就像使用XMLHttpRequest从Yahoo! Web服务获取数据,然后对结果调用
formatImage
的JavaScript代码。它通过让HTTP请求作为浏览器处理HTML标签的副作用发生,绕过了浏览器安全模型。
JoD改变了嵌入在HTML页面中的脚本和通过
<script src="...">
包含的脚本的传统角色。Web浏览器请求一个Web服务URI,以为它只是HTML页面中应用代码最终会调用的JavaScript库,但实际上库函数是本地定义的(这里是
formatImage
),而调用该函数的应用代码来自外部网站。
若在调用Yahoo! Web服务时未在URI中指定回调函数,会得到一个仅包含JSON数据结构的“JavaScript”文件。将该文件包含在
<script>
标签中不会有任何作用,但可以使用可编程的HTTP客户端来获取它。
综上所述,虽然这些绕过浏览器安全模型的技巧能实现一些功能,但也带来了安全风险。目前这仍是一个不成熟的领域,W3C正在努力解决相关问题,目标是在不牺牲安全性、不增加过多复杂性或不将Ajax应用移出网络视野的前提下实现相同的功能。
深入探索Ajax应用:功能重塑、跨浏览器问题与安全挑战
8. 安全方法的困境
有一种安全的方法可以让JavaScript应用获得进行外部Web服务调用的权限,即通过调用
netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
来请求权限。(还有一种不安全的方法,即让用户将Internet Explorer的安全设置调低。)
如果脚本进行了数字签名,客户端的浏览器会向最终用户展示你的凭证。最终用户可以决定是否信任你,若信任则会授予你进行所需Web服务调用的权限。这与之前提到的不可信Web服务客户端试图获得最终用户信任的技术类似,但不同的是,这里的不可信Web服务客户端运行在最终用户信任的Web浏览器内部。
然而,这种安全方法存在两个问题。其一,从
netscape.security.PrivilegeManager
这个名称可以猜到,它仅适用于Mozilla、Firefox和类似Netscape的浏览器。其二,实际设置签名脚本非常麻烦。设置完成后,会发现HTML文件存储在一个签名的Java归档文件中,应用脱离了Web!搜索引擎无法抓取HTML页面,只能通过类似
jar:http://www.example.com/ajax-app.jar!/index.html
这样奇怪的
jar:
URI来访问。
这虽然是正确的解决方案,但也表明该领域尚不成熟。直到最近,Web服务还不够流行,人们没有认真思考这些问题。尽管下面描述的技巧存在潜在危险,但发明者并无恶意,他们只是热衷于浏览器内Web服务客户端的巨大潜力。当前的挑战是找到在不牺牲安全性、不增加过多复杂性或不将Ajax应用移出网络视野的情况下实现相同功能的方法,W3C正在致力于解决这个问题(可参考 “Enabling Read Access for Web Resources”,网址为 http://www.w3.org/TR/access-control/)。
9. 不同技术的安全模型共性
虽然这里主要关注JavaScript应用,但Java applets和Flash也在安全模型的限制下运行,这些安全模型阻止它们向外部服务器发送数据。上述提到的请求代理技巧适用于任何类型的Ajax应用,因为它涉及服务器端的操作。而JoD技巧则是JavaScript特有的。
10. 总结与展望
下面通过表格总结不同技术和方法的特点及问题:
| 技术/方法 | 优点 | 缺点 | 适用场景 |
| — | — | — | — |
| Google Maps式的可寻址性设计 | 恢复应用可寻址性,促进社区发展 | 需额外DOM操作 | 地图类或需要精确定位分享的应用 |
| Prototype库 | 处理跨浏览器问题,简化请求操作 | 隐藏部分功能 | 对跨浏览器兼容性要求高且功能需求相对简单的应用 |
| Dojo库 | 统一API,隐藏多种请求差异 | | 对请求方式多样性有需求的应用 |
| 请求代理 | 绕过跨域限制 | 承担外部网站和客户端请求责任,有安全风险 | 无法直接跨域访问的场景 |
| JoD | 绕过浏览器安全模型进行跨域请求 | 改变脚本传统角色,有安全风险 | 需要跨域获取数据的JavaScript应用 |
| 安全方法(数字签名) | 安全授权 | 兼容性差,设置麻烦 | 对安全性要求极高的应用 |
从流程图可以更清晰地看到不同方法在处理跨域和安全问题时的流程:
graph LR
A[跨域请求需求] --> B{是否使用安全方法}
B -- 是 --> C[数字签名脚本请求权限]
B -- 否 --> D{是否使用库}
D -- 是 --> E{选择Prototype还是Dojo}
E -- Prototype --> F[使用Prototype进行请求]
E -- Dojo --> G[使用Dojo进行请求]
D -- 否 --> H{选择请求代理还是JoD}
H -- 请求代理 --> I[设置服务器代理]
H -- JoD --> J[使用<script>标签获取数据]
C --> K{浏览器是否支持}
K -- 是 --> L[获得权限进行请求]
K -- 否 --> M[无法使用该安全方法]
目前,Ajax应用在跨浏览器和安全方面仍面临诸多挑战,但也有多种方法可供选择。开发者需要根据具体的应用场景和需求,权衡各种方法的优缺点,选择最合适的解决方案。随着技术的发展,相信未来会有更完善、更安全的方法来解决这些问题,让Ajax应用在Web开发中发挥更大的作用。同时,W3C等组织的努力也有望为该领域带来更规范、更安全的标准。
超级会员免费看
1万+

被折叠的 条评论
为什么被折叠?



