1.所谓跨域

跨域是一种浏览器同源安全策略,也即浏览器单方面限制脚本的跨域访问。很多人可能误认为资源跨域时无法请求,实质上请求是可以正常发起的(指通常情况下,部分浏览器存在部分特例),后端也可能正常进行了处理,只是在返回时被浏览器所拦截。可以论证这一点的著名案例就是CSRF跨站攻击。
另外,所谓跨域都是在讨论浏览器行为,包括各种webview容器,其中犹以 XmlHttpRequest 为主。正是由于javascript跑在浏览器之上,所以ajax的跨域成了痛点。

2.跨域形成

请求的url与当前页面不同即产生跨域,除常理上的站点直接性不同(百度域名下访问谷歌资源),同个站点也可以产生跨域:
协议跨域,例如从 http 站点访问 https 站点。
主机跨域,例如从 a.baidu.com 访问 b.baidu.com
端口跨域,例如从80端口的站点访问8080端口的站点。
请求域名和直接请求该域名对应的ip之间也算跨域。
内部判断规则:url首部匹配

1
window.location.protocol + window.location.host

简单性的将协议、主机名和端口号抽出进行对比,不同即跨域,所以也是不会去转化为ip地址的。

3.跨域方案之Jsonp

谈起Jsonp在跨域处理方案中也算鼎鼎大名,这是一种非官方的解决方案,源于浏览器允许一些带src属性的标签跨域,例如iframe、script、img等。而Jsonp即是利用了script加载外部脚本的功能。
例如常规下的请求

1
2
3
4
5
6
7
8
9
10
11
get => http://a.test.com/users
=>>
[{
username : '沐心chen',
sex : '男',
address : '广东深圳'
},{
username : '李彦宏',
sex : '男',
address : '山西阳泉'
}]

由于浏览器的同源策略被阻止,此时前端使用script脚本去加载:

1
<script src="http://a.test.com/users"></script>

显然可以成功请求到,只是单纯的json数据无法使用。此时如果后端介入,返回之前包装成如下形式:

1
2
3
4
5
6
7
8
9
jsonp([{
username : '沐心chen',
sex : '男',
address : '广东深圳'
},{
username : '李彦宏',
sex : '男',
address : '山西阳泉'
}]

对于js而言,这就是一个普通的函数调用

1
jsonp(...params)

那么只要前端定义jsonp这个函数,它就会被执行并传入json数据。

1
2
3
4
var jsonp = function(data){
//输出json
console.dir(data);
}

jsonp跨域的流程走完,只是单纯到这一步还不行,因为它将导致后端无法正确处理非jsonp的请求,所以通常会约定一个参数callback,带上回调的函数名。

1
<script src="http://a.test.com/users?callback=jsonp"></script>

后端得到callback参数时,使用该值包装json数据,否则正常处理。
需要注意的是,处理jsonp的函数必须在window下,也即

1
2
3
window.jsonp = function(data){
console.dir(data);
}

方案虽然可行,但也同时意味着jsonp只能发起get请求,对于post就无能为力了。
知道了原理,使用起来相对还是麻烦,那么如何用js简单封装一个jsonp方案呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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脚本,这些留给大家去发挥(思考一下,如果每个开发者都统一用callback,你可以跨域访问,别人也可以跨域访问,安全上面起不到更好的保障,与后端协议好一个自定义的参数,将能稍微避免一些,当然,所谓安全大都只是防范君子)。
浏览器支持:几乎所有

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,浏览器实际发出的请求头如下:

1
2
3
GET /resources/public-data/ HTTP/1.1
...
Origin: http://my.com

此时浏览器维持判断,当服务端返回的响应头中,存在跨域访问控制属性并匹配本次请求,则跨域成功(正常接收数据)。

1
2
3
HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: http://my.com

这种跨域请求非常简单,只需要后端在返回的响应头中添加Access-Control-Allow-Origin属性并将被允许的站点填入即可(多个站点逗号隔开,允许所有站点则设为*)
预请求
预请求不同于简单请求,它首先会发送一个 OPTIONS 请求到目标站点,以查明该请求是否安全可接受,以防止请求对目标站点的数据造成破坏。当请求具备以下条件,就会被当成预请求处理:
请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为 application/x-www-form-urlencoded , multipart/form-data 或者 text/plain 以外的数据类型。比如说,用 POST 发送数据类型为 application/xml 数据的请求。
使用自定义请求头
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,符合预请求的规范。
此时浏览器与后端的交互过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//浏览器预先发起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请求。
带凭证的请求
跨站请求一般而言,是不会携带cookie和其它凭证的,但 CORS 允许这样做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,否则响应体将被浏览器忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
//浏览器发起请求,携带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 需要现代浏览器的支持,但几乎不用关心这个问题,因为大部分目前仍存活的浏览器都有作出实现,对于前端来说可能最多是设置允许携带凭证,其它的工作就解放到后端了。
浏览器支持:

1
2
3
4
5
6
Destop	Mobile
IE8+ Android2.1
Chrome4+ Safari3.2
firefox3.5+ 其它
Opera12+ ..
Safari4+ ..

(本文完)