Js跨域问题是web开发人员最常碰到的一个问题之一。所谓js跨域问题,是指在一个域下的页面中通过js访问另一个不同域下 的数据对象,出于安全性考 虑,几乎所有浏览器都不允许这种跨域访问,这就导致在一些ajax应用中,使用跨域的web service会成为一个问题。 解决js跨域问题,目前在客户端和服务端都有一些现成的解决方案,但这些方案并不能解决所有问题。下面我们先来看下有哪些常用的解决方案,并针对空间产品 对跨域问题的需求给出一个space自己的解决方案,希望能对其他产品组有借鉴意义。
如何在客户端解决js跨域问题几乎是所有web开发人员会首先考虑的。目前最常用的方法有2种:设置document.domain、通过script 标签加载。
采用这种方法的前提是跨域请求涉及的两个页面必须属于一个基础域(例如都是xxx.com,或是xxx.com.cn),使用同一协议(例如都是 http)和同一端口(例如都是80)。例如,aaa.xxx.com里面的一个页面需要调用bbb.xxx.com里的一个对象,则将两个页面的 document.domain都设置为xxx.com,就可以实现跨域调用了。 另外,需要注意的是,这种方式只能用在父、子页面之中,即只有在用iframe进行数据访问时才有用。
对于浏览器来说,script标签的src属性所指向资源就跟img标签的src属性所指向的资源一样,都是一个静态资源,浏览器会在适当的时候自 动去加 载这些资源,而不会出现所谓的跨域问题。这样我们就可以通过该属性将要访问的数据对象引用进当前页面而绕过js跨域问题。 例如,在space的我的空间项目中,需要在hi域下管理中心页面中随机推荐几个热门模块给用户,由于热门模块的相关信息都在act域下的php模块中维 护,如果直接在hi域下通过ajax请求去获取act域下的推荐模块列表相关信息就出现js跨域问题。解决这个问题的最简单方法就是,在hi域下通过 script标签去访问act域提供的这个http接口:
<script type=”text/javascript” src=”http://act.hi.baidu.com/widget/recommend”><script>
当然,前提是act域的这个http接口必须是返回一段js脚本,如一个json对象数组定义的脚本:
modlist = [ {“modname” : “mod1”, “usernum” : 200, “url” : ” /widget/info/1”}, {“modname” : ”mod2”, “usernum” : 300, ”url” : ” /widget/info/2”}, … ];
但script标签也有一定的局限性,并不能解决所有js跨域问题。script标签的src属性值不能动态改变以满足在不同条件下获取不同数据的需求, 更重要的是,不能通过这种方式正确访问以xml内容方式组织的数据。
从上面的说明可以看到,客户端的解决方案局限性太大,而且对于ajax跨域请求,无论两个域是否属于同个基础域,都无法在客户端加以解决。也就是 说,如果 我们要想在ajax请求中访问其他域下的数据,就只能通过服务端进行处理了。 服务端的解决方案的基本原理就是,由客户端将请求发给本域服务器,再由本域服务器的代理来请求数据并将响应返回给客户端。 最常用的服务器解决方案就是利用web服务器本身提供的proxy功能,如apache和lighttpd的mod_proxy模块。在百度内 部,transmit的分流功能也可以解决部分跨域问题。但这些方法都有一定的局限性,鉴于安全性等问题的考虑,space这边最后开发了一个专门用于处 理跨域请求代理服务的spproxy模块,用于彻底解决js跨域问题。 下面我们将以空间的开放平台为例,简单介绍下如何通过apache的mod_proxy、transmit的分流以及space的spproxy模块来解 决该跨域问题,并简单介绍下spproxy的一些特性、缺点及下一步的改进计划。 空间在展现每个UWA开放模块之前都必须请求该模块的xml源代码以进行解析,每个模块的源代码文件都是存放在act域下的/ow/uwa目录下,那么在 用户空间首页(hi域)中请求该xml文件时就会存在js跨域问题。要解决该问题,只能让js向hi域的web服务器请求xml文件,而hi域web服务 器则通过一定的代理机制(如mod_proxy、transmit分流、spproxy)向act域的web服务器请求文件。
如果apache是2.0系列版本,则可以通过在httpd.conf文件中增加以下配置加以解决:
ProxyRequests Off <Proxy *> Order deny,allow Allow from all </Proxy> ProxyPass /ow/uwa http://act.hi.baidu.com/ow/uwa
其中,ProxyRequests 指令关闭了mod_proxy的正向代理功能而启用反向代理功能,Proxy指令使得该配置对所有访问生效,ProxyPass指令使得对本域的/ow /uwa目录下的任何资源的访问都会在内部被转换为一个对act.hi.baidu.com域下的/ow/uwa目录下对应资源的代理请求。 这样,js就可以直接通过访问http://hi.baidu.com/ow/uwa/0/1/0/10001.xml 获取位于act域下的/ow/uwa/0/1/0/目录下的10001.xml文件。
如果apache是经过百度各产品线修改过的1.3版本,则需要mod_proxy和mod_rewrite模块一起配合来达到同样的目的。首先需要在 httpd.conf中增加以下Location指令:
<Location /ow/uwa> SetHandler proxy-server order allow,deny Allow from all </Location>
这样,对于本域下的/ow/uwa目录下的任何资源的访问都会首先由proxy-server这个handler(mod_proxy模块内部定义 的一个 handler)来处理,但光有这段配置还不行,因为还不proxy-server还不知道应该怎么处理,仅仅知道需要自己处理而已。这时还需要在配置段 中增加一个rewrite规则:
RewriteRule ^/ow/uwa/(.*)$ http://act.hi.baidu.com/ow/uwa/$1?%{QUERY_STRING} [P,L]
Rewrite规则最后的[P,L]表明该rewrite是通过mod_proxy代理过去,而不是通过外部重定向过去。如果去掉P标志,即采用以下 rewrite规则:
RewriteRule ^/ow/uwa/(.*)$ http://act.hi.baidu.com/ow/uwa/$1?%{QUERY_STRING} [L]
则响应返回给客户端时标明的资源uri将是重定向后的uri,在我们的例子中就是act.hi.baidu.com域的uri,则浏览器仍然会出现 js跨 域问题。 以上只是对apache的proxy功能的简单应用,更好更强大的介绍可以参考资料【1】和【2】。 Mod_proxy虽然强大,但我们并没有用它来解决跨域问题。首先,要使用它必须要求我们的每台前端机器都能够访问外网,否则我们就只能将请求代理到其 中一台前端机器上(通过机器名做内网域名进行rewrite或代理),而这显然是不可取的,因为我们的一个域名通常由很多前端机器组成,只代理到其中一台 机器会导致该机器压力与其他机器相比很不均衡,甚至撑不住压力,而给所有前端机器都加访问外网权限又可能会存在一些安全性策略问题(具体原因不清楚,但 op和sa显然是不会赞同这种做法)。其次,由于apache本身并没有很好的防ddos攻击机制,一旦有人通过代理去攻击目标域(比如说我们的竞争对手 的网站),则在目标域的web服务器上看来,攻击者就成了我们了,这样的事情发生时,我们就百口莫辩,跳进黄河也洗不清了。
用过transmit的产品线应该都知道,transmit除了用于防攻击之外,还有一个很重要的功能就是分流。有了分流功能,我们就可以将对特定 url 的访问分发给不同的apache处理,从而实现跨域访问的目的。 还是以空间开放平台的这个例子为例,假设我们的act域在jx机房内由jx-space-act00.jx和jx-space-act01.jx这两台机 器组成,apache的端口为8080,则只要我们在transmit的配置文件transmit_common.conf中增加以下配置:
PP_APACHE_DIR : /ow/uwa/ PP_APACHE0 : jx-space-act00.jx:8080 PP_APACHE1 : jx-space-act01.jx:8080
则重启transmit后,南方用户就可以通过访问http://hi.baidu.com/ow/uwa/0/1/0/10001.xml 而获取http://act.hi.baidu.com/ow/uwa/0/1/0/10001.xml这个url所执行的xml内容,从而解决跨域问 题。如果我们在hi域下的js同时还想异步获取act域下的其他数据,比如说/sys/widget/xxx接口提供的数据,则只需要在 PP_APACHE_DIR配置项中增加一个目录定义:
PP_APACHE_DIR : /ow/uwa/, /sys/widget/
由于旧版本的transmit只支持一个分流,所以不能通过它来同时解决对多个外域的跨域请求问题,同时,要支持旧版本transmit,后端的 apache需要做相应的代码修改和配置才行,这也限制了我们的分流功能不能解决跨非百度域的跨域问题。不过好消息是,gm最近发布的新版本 transmit允许增加n个分流,同时支持后端apache不做任何修改,那么对于旧版本transmit所碰到的限制也就不再存在了,通过它就可以在 一定程度上很好地解决跨域问题了。具体配置方法与旧版本类似,大家可以参考新版本transmit的配置文件做相应修改来实现这个目的。
但是,在space的开放平台系统中,我们并不是通过transmit来解决跨域问题,前面也提到了,transmit只能在一定程度上解决这个问 题。为 什么这么说呢?由于transmit增加分流是需要在修改配置后重启transmit程序的,而且随着分流分支的增加,其性能会不断降低,毕竟每次请求到 来时它都需要遍历所有分流分支以判断应该走哪条分支,而对于开放平台来说,任何一个新的开放模块都有可能会引入一个甚至多个新的外域,这会导致 transmit的分流分支数随着开放模块数量的增加而线性增加,这无论在op运维上还是程序性能上都将是不可接受的。 基于这样的考虑,space在开放平台二期项目中引入了一个新的模块——spproxy模块,用于提供跨域请求代理服务,从而彻底解决了js跨域问题。 从某种意义上讲,spproxy其实就是一个ui,它接收来自apache的请求,并处理该请求获取真正的页面数据,然后返回给apache,再由 apache返回给客户端。Spproxy只接收一个apache命令号(AC_SYS_PROXY : 38),并提供了两个http接口:
/sys/pxy/ajax?url=xxxx和/sys/pxy/xml?url=xxx
其中,/sys/pxy/是可以通过apache配置文件来修改成其他目录名的,url参数就是js希望跨域请求的数据的uri(需要进行url编 码,如 果url中有参数),xml接口与ajax接口的唯一区别是,spproxy会强制将前者返回的内容的Content-Type设为text/xml,而 对于后者,则是外域服务器返回的是何种Content-Type就是何种type。 Apache端只需要增加以下两个配置就可以让spproxy来处理以上两个http接口的请求,当然,前提是所用的apache是经过ns改写过的 apache,目前主要是1.3版本的apache:
CmdNoMap pxy 38 CmdHost pxy 10.23.64.185 20540
其中,pxy就是http接口中的第二个目录名,可以自定义,例如配置里如果写的是proxy,则http接口就是/sys/proxy /ajax?url=xxx和/sys/proxy/xml?url=xxx;38是spproxy能够处理的命令号,可以在编译时修改成其他 值;10.23.64.185 20540是spproxy所在机器的ip和spproxy的侦听端口。 通过以上配置后,hi域下的js就可以通过异步访问http://hi.baidu.com/sys/pxy/xml?url=http: //act.hi.baidu.com/ow/uwa/0/1/0/10001.xml来跨域访问http://act.hi.baidu.com/ow /uwa/0/1/0/10001.xml了。如果跨域访问的资源uri带参数,如http://act.hi.baidu.com/widget /recommend?num=6,则在访问时需要将参数值进行url编码,如http://hi.baidu.com/sys/pxy /xml?url=http%3A%2F %2Fact%2Ehi%2Ebaidu%2Ecom%2Fwidget%2Frecommend%3Fnum%3D6。
Spproxy是一个基于epoll网络模型开发的单进程模块,包含一个数据抓取线程和定时加载线程: 抓取线程 ,对跨域请求进行代理,抓取指定url对应的页面内容并返回给前端,此线程采用epoll模型提高请求处理的并发度 定时加载线程,定时加载域名白名单以及部分可重加载的配置项(如各种超时时间、是否强制指定cache过期时间等) spproxy通过一个域名白名单限制js能够跨域访问的域名以降低安全风险,需要增加一个js能够跨域访问的外域时只需要在spproxy的域名白名单 文件spproxy_domainlist.txt中增加一行即可,5分钟后(具体生效时间可配置)即会生效。 由于采用的是epoll网络模型,spproxy本身能够很好地抵御慢连接攻击,同时,它还具有与space ui同样强大的防攻击功能。 为了减少对外域服务器的请求以提高跨域请求的响应速度,同时又降低外域服务器封杀我们的代理服务的风险,spproxy本身做了一个相对简单的cache 功能。如果外域服务器返回的页面http头中指定了cache过期时间,spproxy就会根据该http头对该页面的cache过期时间算一个比较合理 的过期值并对页面进行cache;如果外域服务器返回的http头中没有指定cache过期时间或要求不进行cache,则spproxy还是会对该页面 进行短期的cache,过期时间可配置。 另外,对于spproxy模块中涉及的大多数超时时间配置及域名白名单都是可以定时重加载的,从而实现线上服务调整参数、增加信任域时无需重启服务作废 cache的目的。 不过,spproxy目前也还存在一些缺点: 返回给spproxy的响应体不能是经过压缩编码的,spproxy在向外域请求时会在http头中标明这一点,这会增加读响应时间和外域网站的带宽消耗 Spproxy目前只是根据外域服务器的http响应头中的Cache-Control字段中的max-age属性计算页面的cache过期时间,而实际 上很多网站返回的cache-control字段并不是通过max-age来标示cache过期时间的 Spproxy目前只支持GET方法,不支持其他http方法,而且,spproxy不支持任意大小的外域页面,但可以通过配置改变它所能接收的页面数据 量的最大值 下一步,spproxy将会在解析http响应头中的cache-control字段方面做些改进以便更加合理地控制spproxy对返回页面的 cache,另外,下一步还将支持通过POST方法进行跨域请求,以提高跨域请求的安全性。