首页 PHP 正文
391

PHP解决跨域访问

      ajax发起的跨域请求一般都会被浏览器所阻止,虽然jsonp能解决一些跨域请求的问题,但是毕竟不是正道,而且只能实现GET跨域。要实现真正地跨域请求,必须在服务端做相应的处理。下面就简单地说一下如何在服务端实现跨域的控制。

在跨域的处理上,浏览器根据请求的不同,分为两种情况进行跨域控制处理。
第一种:简单请求。
第二种:预检请求。

那么,什么样的请求会被浏览器认为是简单请求呢?简单请求满足以下条件:
1、请求方法必须是:GET, POST, HEAD。
2、Content-Type必须是:text/plain, multipart/form-data, application/x-www-form-urlencoded。(很好,看来文件上传也是可以当作简单请求的)
3、首部字段必须在安全首部字段集合内(Fetch规范定义了对CORS安全的首部字段集合),也就是说,除安全首部字段之外,不能出现任何其它字段。
安全的首部字段有:Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width。注意了:
Content-Type虽然在安全首部集合里,但是它的值必须满足条件2。
如果用户加了自定义的首部字段也是不算简单请求的。有些浏览器在Accept,Accept-Language,Content-Language中增加了非标准参数值,这都会被当作非简单请求处理。
简单请求这个概念只适合于ajax请求,对于Fetch来说不存在这个说法。
基于身份认证的字段信息,比如cookie, https认证等,需要特殊控制处理。

Ⅰ简单请求的处理

    对于浏览器来说,发送简单请求与发送域内请求一样,并没有什么大的区别(当然cookie是不会带的),不同的是,浏览器在处理简单请求的响应时,如果服务端没有告之浏览器允许本次跨域请求,那么浏览器不会处理这次响应。
服务端通过什么告之浏览器可以处理跨域请求呢?就是通过Access-Control-Allow-Origin字段。对于简单请求,服务端只要在响应头部增加[Access-Control-Allow-Origin: *] 字段即可: header("Access-Control-Allow-Origin: *"); 这样一来,任何客户端都可以正常请求了。
    上面的设置显然有点不安全,如果我们要限制指定站点的请求才能处理呢?也是有方法的。一个跨域请求会带一个Origin头部字段,形如:Origin: http://xxx.com , 表示这个请求的来源。服务端可以通过 $_SERVER['HTTP_ORIGIN'] 获取到这个来源,我们判断一下这个来源是否在允许站点列表,如果在,我们将这个来源地址加入到Access-Control-Allow-Origin字段返回回去。
代码示例:
$origin = $_SERVER['HTTP_ORIGIN'];
if(!in_array(strtolower($origin), $allowOrigin)){
	header("Status: 404"); //不被允许的请求直接抛出404错误码
	exit;
}
//检验通过的请求可以被服务端正常处理,并告之浏览器允许跨域。
header('Access-Control-Allow-Origin: '.$origin);
/*
do something
*/
以上就是服务端处理简单跨域请求的方法,客户端基本不需要任何额外的设置就可以实现跨域了。

预检请求的处理

    相比较简单请求来说,预检请求会复杂一些。如果一个跨域请求不满足简单请求的条件,那么这个请求会被浏览器分成两个步骤:
步骤1、发起一个预检请求(此请求的Method为OPTIONS(HTTP1.1定义的方法), 不携带任何数据包)向服务端询问本次请求是否允许,得到服务端允许的指令后,执行步骤2.
步骤2、向服务端发送正式的请求。
所以在chrome调试工具中,我们会看到此类请求会发起两次网络请求。(严谨的来说,并不是每一个此类请求都会执行步骤1, 这得取决于服务端在处理步骤1的时候,返回的指令(Access-Control-Max-Age)会告之本次跨域合法请求的时效是多少,在时效之内,是不用再发起预检请求的。)
服务端在处理这类请求时,需要分别处理预检请求和正式请求。
代码示例:
场景:客户端提交了一个POST跨域请求,并携带了自定义首部字段X-PINGOTHER: pingpong,此类请求将会触发预检请求。
后台处理如下:
if($_SERVER['REQUEST_METHOD'] == "OPTIONS")
{
    // 告诉客户端我们支持来自 arunranga.com 的请求并且预请求有效期将仅有20天
    if($_SERVER['HTTP_ORIGIN'] == "http://arunranga.com")
    {
        header('Access-Control-Allow-Origin: http://arunranga.com');
        //告诉客户端我们支持的跨域方法
        header('Access-Control-Allow-Methods: POST, GET, OPTIONS'); 
        //告诉客户端我们支持X-PINGARUNER这个自定义首部字段
        header('Access-Control-Allow-Headers: X-PINGARUNER');//多个用逗号+空格分隔
        //预请求有效期20天
        header('Access-Control-Max-Age: 1728000');
        header("Content-Length: 0");
        header("Content-Type: text/plain");
        exit;
    }else{
        header("HTTP/1.1 403 Access Forbidden");
        header("Content-Type: text/plain");
        echo "You cannot repeat this request";
    }
}elseif($_SERVER['REQUEST_METHOD'] == "POST"){
    /* 通过首先获得XML传送过来的blob来处理POST请求,然后做一些处理, 最后将结果返回客户端
    */
    if($_SERVER['HTTP_ORIGIN'] == "http://arunranga.com")
    {
            $postData = file_get_contents('php://input');
            $document = simplexml_load_string($postData);
            
            // 对POST过来的数据进行一些处理

            $ping = $_SERVER['HTTP_X_PINGARUNER'];
            header('Access-Control-Allow-Origin: http://arunranga.com');
            //header('Access-Control-Allow-Credentials: true'); // xhr.withCredentials=true时需要设置此行
            header('Content-Type: text/plain');
            /*
            do something
            */
    }

Ⅲ 附带身份凭证的请求

    此类请求,需要前后端配合设置参数。
   对于XMLHttpRequest, 客户端需要设置withCredentials为true(xhr.withCredentials=true;),服务端需要发送:Access-Control-Allow-Credentials: true. 并且对于简单请求来说, 不能设置 Access-Control-Allow-Origin: *,这点要注意。fetch设置credentials方法:fetch(url, {credentials: 'include'})

最后,再顺带提一下fetch, fetch是一项实验性的技术,老版本浏览器不支持。它提供了一套访问网络资源的接口,功能很强大。有兴趣可以了解下。这里给出一个小例子:
if(window.fetch){
     var formData = new FormData();
     formData.append("myfield", "myvalue");
     var myHeaders = new Headers();
     var myInit = {
		method: 'POST',
		body: formData,
		headers: myHeaders,
		mode: 'cors',
		cache: 'default'
	};
	var myRequest = new Request('http://xxx.com/cgi', myInit);
	fetch(myRequest).then(function(response){
		console.log(response);
		return response.json();
	}).then(function(responseBody){
		console.log(responseBody);
	});
}

正在加载评论...