开发中涉及到一些跨域问题,从而对同源策略有更深的理解,同源策略是保证Web浏览器安全中最基本的安全功能,无论是前端还是后端,如果对同源策略不了解很容易导致一些开发上的难题。
同源策略
对Javascript来说,满足同源策略可以简单理解为下面的三项必须完全相同:
- 协议相同
- 域名相同
- 端口相同
也就是说,只要两个URL中的协议、域名或端口存在任何一项不同,则被浏览器视为是不同源的。
举个例子,下列与http://test.example.com/page1/index.html
的同源情况
http://test.example.com/page2/index.html # 同源
https://test.example.com/page1/index.html # 不同源,协议不同
http://demo.example.com/page1/index.html # 不同源,域名不同
http://test.example.com:3000/page1/index.html # 不同源,端口不同
注意:http://127.0.0.1:8080/index.html
和 http://localhost:8080/index.html
当然也是不同源的
HTML中的<link>
,<img>
,<script>
,<iframe>
等标签都可以跨域加载资源,它们实际上是发送了一次GET请求,而在Javascript中,浏览器不允许脚本行为的跨域,也就是由XMLHttpRequest
发起HTTP的请求会受到同源策略的限制。所以当需要由AJAX
跨域加载资源时,就会涉及到跨域的处理。
跨域的处理通常有以下两种常用的解决方案。
CORS跨域
CORS
跨域需要后端的解决,其主要原理是由服务端来授权控制谁可以跨域获取资源,这是一种比较安全的解决方案。
Access-Control-Allow-Origin
服务端返回的响应头应该在Access-Control-Allow-Origin
中设置允许接受请求的域,例如这样设置
// 表示允许来自所有站点的跨域请求
Access-Control-Allow-Origin: *
// 表示仅允许来自http://localhost:8000的跨域请求
Access-Control-Allow-Origin: http://localhost:8000
OPTIONS预请求
当一个HTTP请求不是一个简单请求时,浏览器有一个预请求的过程,也就是发送一个OPTIONS
请求,浏览器会预先发送一个OPTIOMS
请求给目的站点,去探测目的站点是否允许来自这个源的站点跨域,这个过程是浏览器自行判断并发起的,不需要我们来写,除了GET请求、HEAD请求、没有传参的POST请求和以表单提交方式发出的POST请求外,其它的任何HTTP请求都会发出预请求去探测是否可以继续。
Access-Control-Allow-Credentials
如果是跨域并携带cookie
的通信则要注意了,当XMLHttpRequest
对象实例中withCredentials
属性为true
的情况下,服务端还必须设置Access-Control-Allow-Credentials:true
来确定是否接受cookie
,如果没有设置这个值,尽管客户端发送了cookie
值过来,服务端也会忽略,并且请求失败。
需要特别注意的是,如果此时响应头中Access-Control-Allow-Origin
的值是*
,此时请求还是会失败的,因为在带有cookie
的HTTP请求中,Access-Control-Allow-Origin
的值不能用*
号通配符,只能设置具体的域名。否则Chrome会报如下错误:
因此只有对于一个不带有credentials
的跨域请求,Access-Control-Allow-Origin
才可以指定为*
,表示允许来自所有域的请求。
Access-Control-Max-Age
响应头还可以设置Access-Control-Max-Age
来告诉客户端这一次预请求的有效期,在有效期内再次跨域访问,浏览器不会预先发送预请求直到时间失效
Access-Control-Max-Age: 864000 # 单位是秒,表示10天内,浏览器对于该域的跨域请求,不需要再发送预请求
Access-Control-Allow-Headers
在会发出预请求的情况下必须配置,指明可以自定义的请求头
Access-Control-Allow-Headers: Content-Type
如果POST请求需要传JSON
格式的参数,前端设置了'Content-Type', 'application/json'
,则服务端必须设置Access-Control-Allow-Headers
,否则也不成功,Chrome会报下面的错:
Access-Control-Allow-Methods
表示允许的请求方式,可以配置多个值
Access-Control-Allow-Methods: POST, GET, OPTION, PUT, DELETE
CORS
方式可以灵活可控地解决跨域问题,并且支持所有类型的HTTP请求,但是它存在一定的局限性,这种方式的兼容性差,只支持IE9+,不过很适合用于移动端。
- 前端代码:
var xhr = new XMLHttpRequest();
var postData = {
name: 'lonica',
email: 'test@126.com'
};
xhr.open('POST', 'http://127.0.0.1:3001/', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(JSON.parse(xhr.responseText));
}
};
xhr.send(JSON.stringify(postData));
- 服务端代码:
var http = require('http');
http.createServer(function(req, res) {
var data = {
name: 'lonica'
};
res.writeHead(200, {
'Content-Type' : 'text/plain',
'Access-Control-Allow-Origin' : 'http://localhost:8000',
'Access-Control-Allow-Methods' : 'GET, POST, OPTIONS, PUT, DELETE, HEAD',
'Access-Control-Allow-Credentials' : true,
'Access-Control-Allow-Headers' : 'Content-Type',
'Access-Control-Max-Age' : '3600'
});
res.end(JSON.stringify(data));
}).listen(3001);
JSONP跨域
JSONP
是JSON with Padding
的简称,这种方式的兼容性好,但是只能处理GET请求的跨域,使用的时候需要特别注意。
我们都知道,浏览器不允许使用XMLHttpRequest
脚本去获取不同域的数据,但是可以通过<script>
,<link>
,<img>
等标签来发起GET请求来获取数据,JSONP
就是利用了这个原理,所以也不难理解JSONP
为什么只能支持GET请求了。
JSONP
的原理:利用<script>
标签去获取一段javascript
代码,这段javascript
代码的内容(由服务端拼接)包含了一个方法的调用,调用方法时所传入的参数就是前端要拿到的数据,前端只需要在这个方法的实现里面处理传入的数据即可,JSONP
需要前后端一起配合,确定方法名由前端传参给后端,并由后端拼接成调用方法的语句再返回给前端。
- 前端代码
<script type="text/javascript">
function callback(data) {
// 获取到的数据
console.log(data);
}
var ele = document.createElement('script');
ele.type = 'text/javascript';
ele.src = 'http://127.0.0.1:3000?callback=callback';
document.body.appendChild(ele);
</script>
- 后端代码实现:
var http = require('http');
var url = require('url');
http.createServer(function(req, res) {
var params = url.parse(req.url, true).query;
var func = params.callback;
var data = {
tags: ['jsonp', 'javascript']
};
res.writeHead(200, {'Content-Type' : 'text/plain'});
res.end(func + '(' + JSON.stringify(data) +')');
}).listen(3000);