JavaScript中的跨域
跨域是什么
跨域就是指从一个域名的网页去请求另一个域名的资源,因为JavaScript同源策略的限制,资源无法获取。比如从www.baidu.com 页面去请求 www.google.com 的资源,这是就要用到跨域请求了。严格一点来说就是:只要 协议,域名,端口有任何一个的不同,就被当作是跨域。详情如下:
特别注意两点:
第一,如果是协议和端口造成的跨域问题“前台”是无能为力的,
第二:在跨域问题上,域仅仅是通过“URL的首部”来识别而不会去尝试判断相同的ip地址对应着两个域或两个域是否在同一个ip上。
浏览器为什么要限制跨域访问
原因当然就是安全问题:如果没有限制跨域访问,那么一个页面就可以随意地访问另外一个网站的资源,那么就有可能在客户完全不知情的情况下出现安全问题。比如下面的操作就有安全问题:
·用户访问http://www.icbc.com.cn/icbc/ ,登陆并进行网银操作,这时一些个人信息就可以通过cookie等方式生成并存放在浏览器中
·假如此时用户去访问其他恶意网站
·这时该恶意网站就可以在它的页面中,拿到银行的cookie,比如用户名,登陆token等,然后发起对http://www.icbc.com.cn/icbc/ 的操作。
· 如果这时浏览器不予限制,并且银行也没有做响应的安全处理的话,那么用户的信息有可能就这么泄露了。
那为什么还要使用跨域呢
有时公司内部有多个不同的子域,比如一个是location.company.com ,而应用是放在app.company.com , 这时想从 app.company.com去访问 location.company.com 的资源就属于跨域。
跨域的几种方式
1、JSONP
虽然浏览器默认禁止了跨域访问,但是跨域访问中有图片、css、javascript脚本文件等是不限制,因此你可以在页面渲染时动态在<script>标签设置src路径,而这个路径返回回来的就是json对象。根据这一点,可以方便地通过创建script节点的方法来实现完全跨域的通信,可以自由执行引入的JS文件中的function(包括操作cookie、Dom等等)。具体的做法可以参考YUI的Get Utility
jsonp是一种跨域通信的手段,它的原理其实很简单:
1、首先是利用script标签的src属性来实现跨域。
2、通过将前端方法作为参数传递到服务器端,然后由服务器端注入参数(我们需要的json数据)之后再返回,实现服务器端向客户端通信。
3、由于使用script标签的src属性,因此只支持get方法
优点是兼容性好,简单易用,支持浏览器与服务器双向通信。缺点是只支持GET请求。
实现流程
1、设定一个script标签
<script src="http://jsonp.js?callback=xxx"></script>
2、callback定义了一个函数名,而远程服务端通过调用指定的函数并传入参数来实现传递参数,将fn(response)传递回客户端
$callback = !empty($_GET['callback']) ? $_GET['callback'] : 'callback'; echo $callback.'(.json_encode($data).)';
3、客户端接收到返回的js脚本,开始解析和执行fn(response)
jsonp简单实现
一个简单的jsonp实现,其实就是拼接url,然后将动态添加一个script元素到头部。
function jsonp(req){ var script = document.createElement('script'); var url = req.url + '?callback=' + req.callback.name; script.src = url; document.getElementsByTagName('head')[0].appendChild(script); }
前端js示例
function hello(res){ alert('hello ' + res.data); } jsonp({ url : '', callback : hello });
服务器端代码
var http = require('http'); var urllib = require('url'); var port = 8080; var data = {'data':'world'}; http.createServer(function(req,res){ var params = urllib.parse(req.url,true); if(params.query.callback){ console.log(params.query.callback); //jsonp var str = params.query.callback + '(' + JSON.stringify(data) + ')'; res.end(str); } else { res.end(); } }).listen(port,function(){ console.log('jsonp server is on'); });
然而,这个实现虽然简单,但有一些不足的地方:
1、我们传递的回调必须是一个全局方法,我们都知道要尽量减少全局的方法。
2、需要加入一些参数校验,确保接口可以正常执行。
可靠的jsonp
(function (global) { var id = 0, container = document.getElementsByTagName("head")[0]; function jsonp(options) { if(!options || !options.url) return; var scriptNode = document.createElement("script"), data = options.data || {}, url = options.url, callback = options.callback, fnName = "jsonp" + id++; // 添加回调函数 data["callback"] = fnName; // 拼接url var params = []; for (var key in data) { params.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key])); } url = url.indexOf("?") > 0 ? (url + "&") : (url + "?"); url += params.join("&"); scriptNode.src = url; // 传递的是一个匿名的回调函数,要执行的话,暴露为一个全局方法 global[fnName] = function (ret) { callback && callback(ret); container.removeChild(scriptNode); delete global[fnName]; } // 出错处理 scriptNode.onerror = function () { callback && callback({error:"error"}); container.removeChild(scriptNode); global[fnName] && delete global[fnName]; } scriptNode.type = "text/javascript"; container.appendChild(scriptNode) } global.jsonp = jsonp; })(this); 使用示例 jsonp({ url : "www.example.com", data : {id : 1}, callback : function (ret) { console.log(ret); } });
2、CORS
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。
简单请求的实现
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin
字段。
下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin
字段。
GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-
开头。
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin
字段的值,要么是一个*
,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true
,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务器不要浏览器发送Cookie,删除该字段即可。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。上面的例子指定,getResponseHeader('FooBar')
可以返回FooBar
字段的值。
withCredentials 属性
上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials
字段。
Access-Control-Allow-Credentials: true
另一方面,开发者必须在AJAX请求中打开withCredentials
属性。
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
但是,如果省略withCredentials
设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials
。
xhr.withCredentials = false;
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie
也无法读取服务器域名下的Cookie。
与JSONP比较
CORS与JSONP的使用目的相同,但是比JSONP更强大。
JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
3、document.domain+iframe
对于主域相同而子域不同的例子,可以通过设置document.domain的办法来解决。
前提条件:这两个域名必须属于同一个基础域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域
具体的做法是可以在http://www.a.com/a.html和http://script.a.com/b.html两个文件中分别加上document.domain = ‘a.com’;然后通过a.html文件中创建一个iframe,去控制iframe的contentDocument,这样两个js文件之间就可以“交互”了。当然这种办法只能解决主域相同而二级域名不同的情况,如果你异想天开的把script.a.com的domian设为alibaba.com那显然是会报错地!代码如下:
www.a.com上的a.html
document.domain = 'a.com'; var ifr = document.createElement('iframe'); ifr.src = 'http://script.a.com/b.html'; ifr.style.display = 'none'; document.body.appendChild(ifr); ifr.onload = function(){ var doc = ifr.contentDocument || ifr.contentWindow.document; // 在这里操纵b.html alert(doc.getElementsByTagName("h1")[0].childNodes[0].nodeValue); };
script.a.com上的b.html
document.domain = 'a.com';
问题:
1、安全性,当一个站点(b.a.com)被攻击后,另一个站点(c.a.com)会引起安全漏洞。
2、如果一个页面中引入多个iframe,要想能够操作所有iframe,必须都得设置相同domain。
还有一点要注意,这个方法需要在iframe加载后才能使用!
4、还有几种方式,暂且不说,未完待续。。。。。。
使用window.name来进行跨域
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的
使用HTML5中新引进的window.postMessage方法来跨域传送数据
还有flash、在服务器上设置代理页面等跨域方式。个人认为window.name的方法既不复杂,也能兼容到几乎所有浏览器,这真是极好的一种跨域方法。
参考: