1. 所谓跨域
跨域是一种浏览器同源安全策略,也即浏览器单方面限制脚本的跨域访问。很多人可能误认为资源跨域时无法请求,实质上请求是可以正常发起的(指通常情况下,部分浏览器存在部分特例),后端也可能正常进行了处理,只是在返回时被浏览器所拦截。可以论证这一点的著名案例就是CSRF跨站攻击。
另外,所谓跨域都是在讨论浏览器行为,包括各种webview容器,其中犹以 XmlHttpRequest
为主。正是由于javascript跑在浏览器之上,所以ajax的跨域成了痛点。
2. 跨域形成
请求的url与当前页面不同即产生跨域,除常理上的站点直接性不同(百度域名下访问谷歌资源),同个站点也可以产生跨域:
- 协议跨域,例如从
http
站点访问https
站点。 - 主机跨域,例如从
a.baidu.com
访问b.baidu.com
- 端口跨域,例如从80端口的站点访问8080端口的站点。
请求域名和直接请求该域名对应的ip之间也算跨域。
内部判断规则:url首部匹配
window.location.protocol + window.location.host
简单性的将协议、主机名和端口号抽出进行对比,不同即跨域,所以也是不会去转化为ip地址的。
3. 跨域方案之Jsonp
谈起Jsonp在跨域处理方案中也算鼎鼎大名,这是一种非官方的解决方案,源于浏览器允许一些带src属性的标签跨域,例如iframe、script、img等。而Jsonp即是利用了script加载外部脚本的功能。
例如常规下的请求
get => http://a.test.com/users
=>>
[{
username : '沐心chen',
sex : '男',
address : '广东深圳'
},{
username : '李彦宏',
sex : '男',
address : '山西阳泉'
}]
由于浏览器的同源策略被阻止,此时前端使用script脚本去加载:
<script src="http://a.test.com/users"></script>
显然可以成功请求到,只是单纯的json数据无法使用。此时如果后端介入,返回之前包装成如下形式:
jsonp([{
username : '沐心chen',
sex : '男',
address : '广东深圳'
},{
username : '李彦宏',
sex : '男',
address : '山西阳泉'
}])
对于js而言,这就是一个普通的函数调用
jsonp(...params)
那么只要前端定义jsonp这个函数,它就会被执行并传入json数据。
var jsonp = function(data){
//输出json
console.dir(data);
}
jsonp跨域的流程走完,只是单纯到这一步还不行,因为它将导致后端无法正确处理非jsonp的请求,所以通常会约定一个参数callback,带上回调的函数名。
<script src="http://a.test.com/users?callback=jsonp"></script>
后端得到callback参数时,使用该值包装json数据,否则正常处理。
需要注意的是,处理jsonp的函数必须在window下,也即
window.jsonp = function(data){
console.dir(data);
}
方案虽然可行,但也同时意味着jsonp只能发起get请求,对于post就无能为力了。
知道了原理,使用起来相对还是麻烦,那么如何用js简单封装一个jsonp方案呢?
var getJsonp = function(url, success){
//声明window下的jsonp函数
window.jsonp = function(data){
//jsonp函数被执行将data转发到success函数
success(data);
}
var src = '';
//判断地址是否带其它参数决定callback怎么拼接
if(url.IndexOf('?') != -1){
src = url + '&callback=jsonp';
}else{
src = url + '?callback=jsonp';
}
//动态创建script标签
var script = document.createElement('script');
script.type = "text/javascript";
script.src = src;
document.head.appendChild(script);
}
//用法
getJsonp('http://test.com/users', function(data){
console.log('得到jsonp数据:',JSON.stringify(data));
});
上面只是一个简单的封装思路,如果需要做的更好可以允许指定callback,还可以在回调函数之后销毁script脚本,这些留给大家去发挥。
浏览器支持:几乎所有
4. 跨域解决方案之CORS
CORS,也即 Cross-Origin Resource Sharing
(跨域资源共享),它需要现代浏览器的支持,是一种更安全的官方解决方案。
CORS使得以下常见场景得到支持:
- 使用 XMLHttpRequest 或 Fetch 发起跨站 HTTP 请求。
- web 字体(css 中通过 @font-face 使用跨站字体资源)
- 使用 drawImage 绘制 Images/video 画面到 canvas
CORS有以下三种常见的访问控制场景:
-
简单请求
- 只使用 GET 、HEAD 或者 POST 发起请求,如果使用 POST ,那么其数据类型( Content-Type )只能是
application/x-www-form-urlencoded
、multipart/form-data
或text/plain
中的一种。 - 不使用自定义请求头
这种请求跟正常的ajax请求几乎没有差异,只是浏览器会在请求头中自动添加一个origin属性,内容为本页面地址。例如我们使用 XMLhttprequest 正常发起一个 GET 请求,源站点为
my.com
,目标站点为test.com
,浏览器实际发出的请求头如下:GET /resources/public-data/ HTTP/1.1 ... Origin: http://my.com
此时浏览器维持判断,当服务端返回的响应头中,存在跨域访问控制属性并匹配本次请求,则跨域成功(正常接收数据)。
HTTP/1.1 200 OK ... Access-Control-Allow-Origin: http://my.com
这种跨域请求非常简单,只需要后端在返回的响应头中添加
Access-Control-Allow-Origin
属性并将被允许的站点填入即可(多个站点逗号隔开,允许所有站点则设为*
) - 只使用 GET 、HEAD 或者 POST 发起请求,如果使用 POST ,那么其数据类型( Content-Type )只能是
-
预请求
预请求不同于简单请求,它首先会发送一个 OPTIONS 请求到目标站点,以查明该请求是否安全可接受,以防止请求对目标站点的数据造成破坏。当请求具备以下条件,就会被当成预请求处理:
- 请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为
application/x-www-form-urlencoded
,multipart/form-data
或者text/plain
以外的数据类型。比如说,用 POST 发送数据类型为application/xml
数据的请求。 - 使用自定义请求头
例:
var request = new XMLHttpRequest(); var url = 'http://test.com/users'; var body = 'test'; function coAccess(){ if(request) { request.open('POST', url, true); request.setRequestHeader('X-CUSTOMER-HEADER', '沐心chen'); request.setRequestHeader('Content-Type', 'application/xml'); request.onreadystatechange = function(state){ ... }; request.send(body); } ...
上面发送了一个 POST 请求,请求数据类型为
application/xml
,并携带一个自定义请求头X-CUSTOMER_HEADER
,符合预请求的规范。此时浏览器与后端的交互过程如下:
//浏览器预先发起OPTIONS请求 ,自动添加Origin、Access-Control-Request-Method和Access-Control-Request-Headers OPTIONS /resources/post-here/ HTTP/1.1 ... Origin: http://my.com Access-Control-Request-Method: POST Access-Control-Request-Headers: X-CUSTOMER-HEADER //后端接收OPTIONS请求,返回响应头中包含Access-Control-Allow-*策略和Access-Control-Max-Age时限 HTTP/1.1 200 OK ... Access-Control-Allow-Origin: http://my.com Access-Control-Allow-Methods: POST, GET, DELETE, UPATE, PATCH, OPTIONS Access-Control-Allow-Headers: X-CUSTOMER-HEADER Access-Control-Max-Age: 1728000 Vary: Accept-Encoding, Origin //浏览器判断本次请求被允许,真实发起原先的POST请求 POST /resources/post-here/ HTTP/1.1 ... X-CUSTOMER-HEADER: 沐心chen Origin: http://my.com //服务器返回数据 HTTP/1.1 200 OK ... Access-Control-Allow-Origin: http://my.com Vary: Accept-Encoding, Origin
OPTIONS
是一个理论上不应该对服务端数据造成影响的请求方式。响应头Access-Control-Allow-Methods
表明服务器可以接受POST
,GET
,DELETE
,UPATE
,PATCH
,OPTIONS
的请求方法,而Access-Control-Max-Age
则告诉浏览器本次预请求的有效期为20天,在这段时间内针对该站点的请求都不需要再预先发起OPTIONS
请求。 - 请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为
-
带凭证的请求
跨站请求一般而言,是不会携带
cookie
和其它凭证的,但 CORS 允许这样做。var request = new XMLHttpRequest(); var url = 'http://test.com/users'; function coAccess(){ if(request) { request.open('GET', url, true); request.withCredentials = true; request.onreadystatechange = function(state){ ... }; request.send(body); } ...
我们在
request
中将withCredentials
设置为true
,使得该请求携带cookie
和凭证,此时服务端必须在响应头中声明Access-Control-Allow-Credentials
为true
,否则响应体将被浏览器忽略。//浏览器发起请求,携带cookie信息 GET /resources/access-control-with-credentials/ HTTP/1.1 ... Origin: http://my.com Cookie: rememberMe=沐心chen //服务端返回,设置了更多cookie HTTP/1.1 200 OK ... Access-Control-Allow-Origin: http://my.com Access-Control-Allow-Credentials: true Vary: Accept-Encoding, Origin Set-Cookie:rememberYou=沐心chen
值得一提的是,带凭证的请求要求服务端具体设置
Access-Control-Allow-Origin
的值而不允许使用*
,否则响应也会被浏览器忽略。如果一切正常,跨域访问将同时允许cookie
的读和写。
上面一直没提的一个响应头属性是 Vary
,顺带提及一下,如果我们的跨域方案不需要cookie参与,那么Access-Control-Allow-Origin
是允许设置为 *
的,但如果我们具体的去设置它的允许域名,则需要后端在响应头再设置一个 Vary
参数,值为 Accept-Encoding, Origin
,它告诉浏览器,响应是根据请求头里的Origin的值来返回不同的内容的。
尽管 CORS
需要现代浏览器的支持,但几乎不用关心这个问题,因为大部分目前仍存活的浏览器都有作出实现,对于前端来说可能最多是设置允许携带凭证,其它的工作就解放到后端了。
浏览器支持:
Destop | Mobile |
---|---|
IE8+ | Android2.1 |
Chrome4+ | Safari3.2 |
firefox3.5+ | 其它 |
Opera12+ | .. |
Safari4+ | .. |
5. 结言
本篇文章仅针对前端跨域解决方案,虽然不同站点间传输数据方案不少,但只有真正的痛点需要拿出来剖析,零零散散的不再赘述,如需了解更多可以自行搜索。后端对跨域的处理也许抽时间还会独立写一篇文章解读,感兴趣的朋友走个关注,眼熟ID,不胜感激。
发现挺多站点自行转载文章,也并没有注明出处,写文章并不容易,希望以后可以在转载的同时注明一下来源,稍微尊重别人的劳动成果,谢谢。