跨源通信、跨域访问
跨源通信、跨域访问
什么是跨源、跨域
所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol
),主机(host
)端口号(port
)。
同源策略是浏览器的一个安全功能,不同源的客户端脚本在未明确授权的情况下,不能读写对方的资源。
同源策略 是浏览器安全的基石。
同源策略会阻止一个域的 javascript
脚本和另外一个域的内容进行交互,例如办公内外网环境,当我们访问外网一个恶意网站的时候,恶意网站就会利用我们的主机向内网的 url 发送 ajax 请求,破坏或盗取数据。
浏览器的非同源限制、解决思路
非同源限制
- 无法读取非同源网页的
Cookie
、LocalStorage
和IndexedDB
; - 无法操作非同源网页的
DOM
; - 无法向非同源地址发送
AJAX
请求,即XHR
请求。
跨域的解决思路-3种
避免非同源限制—“躲着走”
- 让浏览器不做限制,指定参数,让浏览器不做校验,但该方法不太合理,因为它需要每个用户都去对浏览器设置做改动;
- 不要发出 XHR 请求,这样就算是跨域,浏览器也不会有非同源限制,解决方案是
JSONP
,通过动态创建一个script
,通过script
发出请求;
跨域资源共享方案
根据 W3C 的跨源资源共享方案,在被调用方修改代码,加上字段,告诉浏览器该网站支持跨域。
通过服务器反向代理,隐藏跨域
使用 Nginx
反向代理,在 a
域名中的请求地址使用反向代理指向 b
域名,让浏览器一直以为在访问 a
网站,不触发跨域限制。
JSONP
简单示例(JavaScript + PHP)
JavaScript
<script>
var url="http://b.cn/do.php?callback=foo";
//注意 foo函数的定义要先于 引入 url里的内容
function foo(res){
console.log(res);
console.log(res.a);
}
function loadData(url){
var elem=document.createElement('script');
elem.src=url;
document.getElementsByTagName('head')[0].appendChild(elem);
}
loadData(url);
</script>
PHP
$arr=['a'=>'ajax','b'=>'bbc'];
$callback=$_GET['callback'];
$json=$callback."(".json_encode($arr).");";
echo $json; //foo({"a":"ajax","b":"bbc"});
上述代码中,相当于使用 PHP
结合前端的 JavaScript
拼接成一个 foo
方法的调用,也就是 loadData
方法最后返回的是 foo
函数的一个调用,也就是 foo({"a":"ajax","b":"bbc"})
;
Jquery中 JSONP 示例
这里使用到 Jquery
中两个参数:
Jsonp
:回调函数的参数名,默认为callback
,服务端凭借这个参数获得回调函数名称;JsonpCallback
:jsonp
请求成功后,在js
中调用的回调函数的名字,默认是Jquery
自动生成。指定jsonpCallback
时可以将回调函数写在Ajax
外面做其他操作,不指定时不能这样做,只能在SUCCESS
里做操作未指定 Jsonp、JsonpCallback 的名称
JavaScript
$.ajax({
url: 'http://lifeloopdev.info/get_events',
dataType: "jsonp",
data: "offset=0&num_items=10",
success: function (data) {
$.each(data.success, function (i, item) {
$("body").append('<h1>' + item.title + '</h1>');
});
}
});
PHP
header('content-type:text-html;charset=utf-8');
$callback = trim($_GET['callback']);
echo $callback + "{ \"success\": [{ \"id\": 1, \"title\": \"title 1\" }, { \"id\": 2, \"title\": \"title 2\" }, { \"id\": 3, \"title\": \"title 3\"}] }";
指定 Jsonp、JsonpCallback 的名称
JavaScript
$.ajax({
url: 'http://lifeloopdev.info/get_events',
dataType: "jsonp",
data: "offset=0&num_items=10",
jsonp: "selfCallback",
jsonpCallback: 'successCallback'
});
function successCallback(data) {
$.each(data.success, function (i, item) {
$("body").append('<h1>' + item.title + '</h1>');
});
}
PHP
header('content-type:text-html;charset=utf-8');
$callback = trim($_GET['selfCallback']);
echo $callback + "{ \"success\": [{ \"id\": 1, \"title\": \"title 1\" }, { \"id\": 2, \"title\": \"title 2\" }, { \"id\": 3, \"title\": \"title 3\"}] }";
JSONP 的弊端
- 需要服务器改动代码;
- 只支持 GET 请求;
- 发送的不是 XHR请求;
- 相对不安全。
后端解决跨域
跟用户数据有关的就是动态请求,没有数据的是静态请求,比如 css js,So,HTTP 服务器(Apache、Nginx 等)至少做了两个作用
- HTTP 服务器,处理静态请求;
- 反向代理,负载均衡。
在服务器端解决跨域有2种解决思路:
- 在被调用后端应用解决:在响应头增加指定字段,告诉浏览器,允许调用,这种解决方案的请求是直接从浏览器发送给后端服务器,在浏览器上会看到 b.com 的 url。
- 在前端服务器解决:这是隐藏跨域的解决方案。这种跨域请求不是直接从浏览器发送的,而是从中间的 http 服务器(前端应用所在服务器)转发过去的,在浏览器中看到的还是 a.com 的 url,所以不会认为是跨域,但是该到 b.com 的请求还是会到 b.com。
CORS解决跨域相关原理
为了解决浏览器跨域问题,W3C 提出了跨源资源共享方案,即 CORS(Cross-Origin Resource Sharing)。
CORS 可以在不破坏即有规则的情况下,通过后端服务器实现 CORS 接口,就可以实现跨域通信。
CORS 将请求分为两类:简单请求和非简单请求,分别对跨域通信提供了支持。
简单请求
简单请求就是普通 HTML Form 在不依赖脚本的情况下可以发出的请求,比如表单的 method 如果指定为 POST ,可以用 enctype 属性指定用什么方式对表单内容进行编码,合法的值就是前述这三种。
- 在 CORS 出现前,发送 HTTP 请求时在头信息中不能包含任何自定义字段,且 HTTP 头信息不超过以下几个字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type 仅为这3种
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 请求方法是 GET HEAD POST 且满足条件1
一个简单请求的🌰
GET /test HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, sdch, br
Origin: http://www.test.com
Host: www.test.com
对于简单请求,CORS 的策略是请求时在请求头中增加一个 Origin 字段,表示请求发出的域。
服务器收到请求后,根据该字段判断是否允许该请求访问。
- 如果允许,则在 HTTP 头信息中添加 Access-Control-Allow-Origin 字段,并返回正确的结果
- 如果不允许,则不添加 Access-Control-Allow-Origin 字段
除了上面提到的 Access-Control-Allow-Origin,还有几个字段用于描述 CORS 返回结果
- Access-Control-Allow-Credentials:可选,用户是否可以发送、处理cookie
- Access-Control-Expose-Headers:可选,可以让用户拿到的字段。有几个字段无论是否允许跨域都可以拿到的:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
非简单请求
一般是发送 JSON 格式的 ajax 请求,或带有自定义头的请求。
非简单请求就是普通 HTML Form 无法实现的请求。比如 PUT 方法、需要其他的内容编码方式、自定义头之类的。
对于非简单请求的跨源请求,浏览器会在真实请求发出前,增加一次 OPTION 请求,称为预检请求(preflightrequest)。
预检请求将真实请求的信息,包括请求方法、自定义头字段、源信息添加到 HTTP 头信息字段中,询问服务器是否允许这样的操作。
例如一个 GET 请求的预检请求,包含一个自定义参数X-Custom-Header。
OPTIONS /test HTTP/1.1
Origin: http://www.test.com
Access-Control-Request-Method: GET // 请求使用的 HTTP 方法
Access-Control-Request-Headers: X-Custom-Header // 请求中包含的自定义头字段
Host: www.test.com
服务器收到请求时,需要分别对 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 进行验证,验证通过后,会在返回 HTTP 头信息中添加:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://www.test.com // 允许的域
Access-Control-Allow-Methods: GET, POST, PUT, DELETE // 允许的方法
Access-Control-Allow-Headers: X-Custom-Header // 允许的自定义字段
Access-Control-Allow-Credentials: true // 是否允许用户发送、处理 cookie
Access-Control-Max-Age: 172800 // 预检请求的有效期,单位为秒。有效期内,不需要发送预检请求,ps 48小时
当预检请求通过后,浏览器才会发送真实请求到服务器。这样就实现了跨域资源的请求访问。
所以后端处理其实处理的就是这次预检后请求。
<?php
header('Content-Type: text/html;charset=utf-8');
header('Access-Control-Allow-Origin:*'); // *代表允许任何网址请求
header('Access-Control-Allow-Methods:POST,GET,OPTIONS,DELETE'); // 允许请求的类型
header('Access-Control-Allow-Credentials: true'); // 设置是否允许发送 cookies
header('Access-Control-Allow-Headers: Content-Type,Content-Length,Accept-Encoding,X-Requested-with, Origin'); // 设置允许自定义请求头的字段
if($_GET['name']) {
$name = $_GET['name'];
echo $name;
} else {
echo "请求成功但。。。。";
} ?>
预检请求—CORS-preflight
对于服务器来说:
第一,许多服务器压根没打算给跨源用。当然你不给 CORS 响应头,浏览器也不会使用响应结果,但是请求本身可能已经造成了后果。所以最好是默认禁止跨源请求。
第二,要回答某个请求是否接受跨源,可能涉及额外的计算逻辑。这个逻辑可能很简单,比如一律放行。也可能比较复杂,结果可能取决于哪个资源哪种操作来自哪个 origin。对浏览器来说,就是某个资源是否允许跨源这么简单;对服务器来说,计算成本却可大可小。所以我们希望最好不用每次请求都让服务器劳神计算。
CORS-preflight 就是这样一种机制,浏览器先单独请求一次,询问服务器某个资源是否可以跨源,如果不允许的话就不发实际的请求。注意先许可再请求等于默认禁止了跨源请求。如果允许的话,浏览器会记住,然后发实际请求,且之后每次就都直接请求而不用再询问服务器否可以跨源了。于是,服务器想支持跨源,就只要针对 preflight 进行跨源许可计算。本身真正的响应代码则完全不管这个事情。并且因为 preflight 是许可式的,也就是说如果服务器不打算接受跨源,什么事情都不用做。
但是这机制只能限于非简单请求。
在处理简单请求的时候,如果服务器不打算接受跨源请求,不能依赖 CORS-preflight 机制,因为不通过 CORS,普通表单也能发起简单请求,所以默认禁止跨源是做不到的。
既然如此,简单请求发 preflight 就没有意义了,就算发了服务器也省不了后续每次的计算,反而在一开始多了一次 preflight。
简单请求为何不用预检请求?
有些人把简单请求不需要 preflight 理解为『向下兼容』。这也不能说错。但严格来说,并不是『为了向下兼容』而不能发。理论上浏览器可以区别对待表单请求和非表单请求 —— 对传统的跨源表单提交不发 preflight,从而保持兼容,只对非表单跨源请求发 preflight。
但这样做并没有什么好处,反而把事情搞复杂了。比如本来你可以直接用脚本发跨源普通请求,尽管(在服务器默认没有跨源处理的情况下)你无法得到响应结果,但是你的需求可能只是发送无需返回,比如打个日志。但现在如果服务器不理解 preflight 你就干不了这个事情了。
而且如果真的这样做,服务器就变成了默认允许跨源表单,如果想控制跨源,还是得(跟原本一样)直接在响应处理中执行跨源计算逻辑;另一方面服务器又需要增加对 preflight 请求的响应支持,执行类似的跨源计算逻辑以控制来自非表单的相同跨源请求。服务器通常没有区分表单/非表单差异的需求,这样搞纯粹是折腾服务器端工程师。
所以简单请求不发 preflight 不是因为不能兼容,而是因为兼容的前提下发 preflight 对绝大多数服务器应用来说没有意义,反而把问题搞复杂了。
注意
在 Chrome 和 Firefox 中,如果 Access-Control-Allow-Methods 中并未允许 GET/POST/HEAD 请求,但允许跨域了,浏览器还是会允许 GET/POST/HEAD 这些 简单请求 访问,这时就必须在后台用其他办法禁掉这些 Method。
后端服务器–Nginx 解决方案
这里的 Nginx 仅做反向代理功能,浏览器访问页面在 a.com
的 Nginx 上,ajax 请求接口是 b.com
,所以浏览器认为是跨域
Nginx在 nginx.conf
上配(vhost 是约定做法,这样做不修改主文件)
include vhost/*.config;
创建 cors.conf
server{
listen 80; // 监听80端口
server_name b.com; // 监听向 b.com 发送的请求
location /{
proxy_pass http://ser432ver.53253bb.com:8080; // 转发到哪里
add_header Access-Control-Allow-Origin $http_origin; // $http_ 可以获取请求中相应的 header 参数
add_header Access-Control-Allow-Method *;
add_header Access-Control-Allow-Headers X-Custom-Header;
// 或者
// add_header Access-Control-Allow-Headers $http_access_control_request_headers;
add_header Access-Control-Allow-Credentials true;
add_header Access-Max-age 172800;
// 直接处理预检命令,if 后要带空格
if ($request_method = OPTIONS) {
return 200;
}
}
}
前端服务器解决方案
但其实大部分情况下,我们会把前端应用和请求转发放在同一台 Nginx 上
server{
listen 80; // 监听80端口
server_name a.com; // 监听向 a.com 发送的请求
location / {
root html;
index index.html index.htm;
}
locltion /ajaxserver {
proxy_pass http://ser432ver.53253bb.com:8080; // 后端地址
}
}
这样实质是隐藏跨域,让浏览器认为没有访问其他域就不会发生跨域。
前端代码需要在每个 ajax 请求前都要加上/ajaxserver
。
参考资料
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: