Jade Dungeon

Ajax跨域请求

背景

跨源HTTP请求(也称跨域AJAX请求)是大多数Web开发人员可能遇到的一个问题,根据 同源策略,浏览器将限制客户端的JavaScript在一个安全沙箱内,通常JS不能直接同一台 不同的域的远程服务器通信(域名、协议、端口必须相同)。在过去, 开发者们创造了许多解决方法以实现跨域资源请求,常用的方法如下:

  1. 使用Flash/Silverlight或服务器端「代理」来与远程通讯
  2. 带填充JSON (JSONP).
  3. 在iframe中嵌入远程服务器并通过fragment或window.name通信,参考这里。

如此等等..

这些解决方法或多或少都有问题,比如使用JSONP时若只是简单的「eval」将导致安全漏洞。 方法3虽然能用,但两个域间必须依据严格的协议,恕我直言它既不灵活也不优雅。

跨域资源共享(CORS)

W3C已经引入了跨域资源共享(CORS)作为能够解决该问题并提供安全、灵活以及推荐标准 的解决方案。

机制

从较高的层次来看我们可以简单认为CORS是介于域A客户端的AJAX调用和一个托管在域B的 页面之间的契约, 一个典型的跨源请求或者响应将会是这样:

域 A 的 AJAX 请求头:

Host DomainB.com  
User-Agent Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0) Gecko/20100101 Firefox/4.0  
Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json  
Accept-Language en-us;  
Accept-Encoding gzip, deflate  
Keep-Alive 115  
Origin http://DomainA.com 

域 B 的 响应头:

Cache-Control private /> Content-Type application/json; charset=utf-8  
Access-Control-Allow-Origin DomainA.com  
Content-Length 87  
Proxy-Connection Keep-Alive  
Connection Keep-Alive

上面标记的部分是关键实现, "Origin" 请求头表示跨源请求或者预检请求源于哪里, Access-Control-Allow-Origin请求头表示这个页面允许来自域A的请求(其值为*表示 允许任何域的远程请求)。

像我上面提到的,W3 建议浏览器在提交实际跨源HTTP 请求前,实现「预检请求」, 简而言之,就是一个HTTP OPTIONS 请求:

OPTIONS DomainB.com/foo.aspx HTTP/1.1

如果foo.aspx支持OPTIONS HTTP 指令, 它可能会像下面这样返回响应:

HTTP/1.1 200 OK  
Date: Wed, 01 Mar 2011 15:38:19 GMT  
Access-Control-Allow-Origin:   http://DomainB.com  
Access-Control-Allow-Methods: POST, GET, OPTIONS, HEAD  
Access-Control-Allow-Headers: X-Requested-With  
Access-Control-Max-Age: 1728000  
Connection: Keep-Alive  
Content-Type: application/json

只有满足在响应中包含Access-Control-Allow-Origin,并且其值为*或者包含提交 CORS请求的域,这些强制条件的浏览器才能提交正式的跨域请求, 并在预检结果缓存中 缓存请求结果 。

方案1:JSONP

简单来说,原来的JSON返回的是一个Json:

{"username" : "adda"}

而在JSONP中,返回的是一段函数调用的代码:

myFunction({"username" : "adda"})

其中函数的名称myFunction在每次调用时都不一样,就像每个HTTP请求中的SESSIONID 都不一样。具体的名称由HTTP请求中的callback参数给出

http://www.api.com/myfunction?param=aa&callback=jsonp1

服务器端的实现

作为接收请求的服务器端,要根据请求中的jsonp参数,把数据包装了函数调用:

Java Servlet

this.getRequest().getParameter("callback");  
String jsoncallback = callback + "({'result':"+result+"})";  
PrintWriter out =  this.getResponse().getWriter();  
out.print(jsoncallback);  
out.flush();  
out.close();  

ASP.NET (C#)

protected void Page_Load(object sender, EventArgs e) {
	String data = String.Empty;
	String returnJSONStr = String.Empty;
 
	switch (Request.HttpMethod) {
		case "GET":
			data = Request.QueryString["Data"];
			returnJSONStr = "{\"Data\":\"Hi remote friend, you tried to passed me data: *" + data + "* through HTTP GET.\"}";
			break;
		case "POST":
			data = Request.Form["Data"];
			returnJSONStr = "{\"Data\":\"Hi remote friend, you tried to POST some mock data: *" + data + "* to me.\"}";
			break;
		case "OPTIONS":
			break;
		default:
			returnBadRequestResponse();
			break;
	}
 
	if (String.IsNullOrEmpty(data))
		returnBadRequestResponse();
	else {
		Response.AddHeader("Access-Control-Allow-Origin", "*");
		Response.ContentType = "application/json";
		Response.Write(returnJSONStr);
	}
}
 
private void returnBadRequestResponse() {
	Response.StatusCode = 400;
	Response.ContentType = "application/json";
	Response.Write("{\"Error\":\"Bad HTTP request type!\"}");
}

PHP

if(isset($["Data"])) { 
  $method=$_SERVER['REQUEST_METHOD']; 
  $data=""; 
  if($method=="POST") { 
 	 $data=$_POST["Data"]; 
 
 	 $fakeData=new FakeData(); 
 	 $fakeData->Data="Hi remote friend, you tried to POST some mock data: *"+data+"* to me."; 
 	 $fakeData->Time=new DateTime("now"); 
  } elseif($method=="GET") { 
 	 $fakeData=new FakeData(); 
 	 $fakeData->Data="Hi remote friend, you tried to passed me data: *"+data+"* through HTTP GET."; 
 	 $fakeData->Time=new DateTime("now"); 
  } else { 
 	 RaiseError(); 
  } 
 
  header('Content-type: application/json'); 
  $jsonStr= json_encode($fakeData); 
  echo($jsonStr); 
} else { 
  RaiseError(); 
} 
 
function RaiseError() { 
  http_send_status(405); 
  header("Status: 405 Method Not Allowed"); 
} 
 
/*Classes definition*/ 
class FakeData { 
  public $Data; 
  public $Time; 
}

客户端的实现

作为发出请求的客户端:

Javascript

客户端AJAXY发起请求代码:

var cor = null; // cor stands for Cross-Origin request
  
if (window.XMLHttpRequest) {
	cor = new XMLHttpRequest();
}
// } else if (window.XDomainRequest) {
// 	cor = new XDomainRequest();
// }
} else {
	alert("Your browser does not support Cross-Origin request!");
	return;
}
 
cor.onreadystatechange = function () {
	if (cor.readyState == 4) {
		document.getElementById('lbl').innerHTML = cor.responseText;
	}
};
 
var data = 'Some fake data';
if (method == 'POST') {
	cor.open('POST', 'http://WayneYe.com/Demo/CORSDemo/CORSDemoServer.aspx', true);
	cor.withCredential = "true";
	cor.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
	cor.send('Data=' + data);
} else if (method == 'GET') {
	cor.open('GET', 'http://WayneYe.com/Demo/CORSDemo/CORSDemoServer.aspx?Data=' + data, true);
	cor.withCredential = "true";
	cor.send(null);
}

JS代码适用于所有主流浏览器(IE8+, FF 3.6+, Chrome 8+),我没有用IE8所采用的 XDomainObject,因为 IE8+, FF and Chrome, Safari等浏览器支持XMLHTTP请求。而且 XDomainObject(XDR)似乎有很多限制(参考: http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx)

jQuery getJSON

jQuery库中已经有了现成的包装:

// fetch data from another domain with JSONP
$.getJSON('//example.com/awesome.json?callback=?', function(remoteData){
  console.log(remoteData)
})

callback=?中的问题会被替换为生成的回调函数名 (如果是一个匿名函数,jQuery 会自动生成一个带时间戳的函数名)。

jQuery ajax

$.ajax({
    dataType:'jsonp',
    jsonp:'jsonp_callback',
    url:'http://www.baidu.com/xxx.jsonp',
    success:function(){
        //dosomthing
    }
});

原生Javascript

添加<script>标签的方法:

function addScriptTag(src){
    var script = document.createElement('script');
    script.setAttribute("type","text/javascript");
    script.src = src;
    document.body.appendChild(script);
}

 window.onload = function(){
     //将自定义的回调函数名result传入callback参数中
     addScriptTag("localhost/bns-relation/index.php?r=BnsRelation/BnsOfProd&callback=result");

}
 //自定义的回调函数result
 function result(data) {
     //我们就简单的获取数据
    console.log(data);
 }

callback=?这个是正如其名表示回调函数的名称, 也就是将你自己在客户端定义的回调函数的函数名传送给服务端, 服务端则会返回以你定义的回调函数名的方法, 将获取的json数据传入这个方法完成回调。

解决方案2:打开跨域限制

服务端设置Access-Control-Allow-Origin

这种方式只要服务端把response的header头中设置Access-Control-Allow-Origin 为制定可请求当前域名下数据的域名即可。一般情况下设为即可。 这样客户端就不需要使用jsonp来获取数据。 关于Access-Control-Allow-Origin设为是否会有安全问题,知乎上有个讨论。

http://www.zhihu.com/question/22992229

浏览器支持

Access-Control-Allow-Origin是html5新增的一项标准,IE10以下是不支持的, 所以如果产品面向的是PC端,就要使用服务端代理或jsonp。

参考资料

跨源资源共享

HTTP权限控制

IIS Verbs configuration