解决跨域问题
介绍一下如何解决跨域问题。
目录:
为什么会有跨域问题
为了阻止一个页面上的恶意脚本通过页面的DOM对象获得访问另一个页面上敏感信息的权限,浏览器采用了同源策略。
在这个策略下,只有在两个页面有相同的源时,web 浏览器允许一个页面的脚本访问另一个页面里的数据。
浏览器的同源策略又分为两种:
- DOM同源策略:禁止对不同源页面DOM进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
- XmlHttpRequest同源策略:禁止使用XHR对象向不同源的服务器地址发起HTTP请求。
同源策略限制的范围:
- 无法读取Cookie、LocalStorage 和 IndexDB 。
- 无法获取DOM 。
- 不能发送 Ajax 请求。
判断是否同源
假设一个 URL 为 http://www.example.com/dir/page.html
,以其为例判断以下 URL 是否同源:
URL 对比 | 结果 | 原因 |
---|---|---|
http://www.example.com/dir/page2.html | 同源 | 相同的协议,主机和端口 |
http://www.example.com/dir2/other.html | 同源 | 相同的协议,主机和端口 |
http://username:password@www.example.com/dir2/other.html | 同源 | 相同的协议,主机和端口 |
http://www.example.com:81/dir/other.html | 不同源 | 相同的协议和主机但端口不同 |
https://www.example.com/dir/other.html | 不同源 | 协议不同 |
http://en.example.com/dir/other.html | 不同源 | 不同主机 |
http://example.com/dir/other.html | 不同源 | 不同主机(需要精确匹配) |
http://www.example.com:80/dir/other.html | 待定 | 端口明确,依赖浏览器实现 |
JSONP实现跨域
jsonp 利用了 <script>
标签中 src 属性能够跨域访问的特性,先定义了一个回调方法,然后将其当作 url 参数的一部分发送到服务端,服务端通过字符串拼接的方式将用户想要的数据包裹在回调方法中,再传回来,返回的 js 脚本直接就会执行了。
<script type="text/javascript">
// 定义一个回调函数函数
function callback(data) {
console.log(data);
};
// 创建一个脚本,并且告诉服务端端回调函数名叫callback
var script = document.createElement('script');
var url = 'http://localhost:3000/jsonp?callback=callback';
script.setAttribute("type","text/javascript");
script.src = url;
document.body.appendChild(script);
</script>
使用 node.js 写服务端代码响应请求:
/* GET jsonp listing. */
router.get('/', function(req, res, next) {
// 要返回的数据
var data = {
"name": "kaelyn"
};
// 把json数据转化成字符串,方便字符串拼接
data = JSON.stringify(data);
// 拼接回调函数的字符串
var callback = req.query.callback+'('+data+');';
res.end(callback);
});
这样就实现了通过 jsonp 跨域访问:
值得注意的是,回调函数需要是全局的。
req.query.callback 获取 URL 的键名为‘callback’ 的查询参数串。
关于 node.js 的 Express 框架的基本使用可以看看这里。
在前端除了上面的一种写法之外,还有其他写法:
<script type="text/javascript">
function callback(data) {
console.log(data);
};
</script>
<!--直接插入一个 script 标签-->
<script type="text/javascript" src="http://localhost:3000/jsonp?callback=callback"></script>
还可以使用 jquery 来实现:
<script type="text/javascript">
$.ajax({
type:"get",
url:"http://localhost:3000/jsonp?",
dataType: "jsonp",
jsonp: "callback", //在一个jsonp请求中重写回调函数的名字(key)
jsonpCallback:"showData", //为jsonp请求指定一个回调函数名(value)
async:false,
success:function(data){
console.log(data);
}
});
</script>
在 AJAX 请求设置中,jsonp 和 jsonpCallback 选项可以不写,jquery都会帮我们写好的,默认直接回调调用请求成功后的回调函数 success 。
如果像上面的代码一样定义,则在服务端接收到的请求 URL 将会加上 ?&callback=showData
,如果我们改了AJAX 请求设置中 jsonp 的值,那么服务端也需要做出一些修改:
var callback = req.query.callback+'('+data+');';
上面代码中的 req.query.callback
要进行相应的修改,因为 URL 中的参数串的键名已经改了,如果还是原来的代码将会获取到 undefined。
虽然是使用了 jquery 帮我们封装好的方法,但是 jsonp 和 ajax 在本质上是不一样的东西,ajax 的核心是通过 XmlHttpRequest 获取非本页内容,而 jsonp 的核心则是动态添加
<script>
标签来调用服务器提供的 js 脚本。
CORS(跨源资源分享)
CORS(Cross-origin resource sharing)是一个W3C标准,是跨源AJAX请求的根本解决方法。
在服务端启用CORS:
//设置跨域访问
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By",' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
上面代码的all方法表示所有请求都必须通过该中间件,参数中的“*”表示对所有路径有效。
在网页中发起 ajax 请求:
$.ajax({
type:"get",
url:"http://localhost:3000/query",
dataType: "json",
success:function(data){
console.log(data);
}
});
结果当然是能成功访问啦:
可以做个比较,如果没有在服务端没有加上上面的允许其他源访问的代码,浏览器将会报错:
服务端返回的 Access-Control-Allow-Origin: * 表明,该资源可以被任意外域访问。如果服务端仅允许来自 http://foo.example
的访问,该首部字段的内容如下:Access-Control-Allow-Origin: http://foo.example
。
如果希望允许多个域名访问,则可以这样设置:
app.all('*', function(req, res, next) {
// 允许访问的域名列表
var originList = ["http://localhost:8020", "https://www.baidu.com", "http://www.google.com"];
// 访问的域名
var reqOrigin = req.headers.origin;
// 判断访问的域名是否在允许访问的域名列表中
if(!!reqOrigin && originList.indexOf(reqOrigin) != -1){
console.log(reqOrigin);
res.header("Access-Control-Allow-Origin", reqOrigin);
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By",' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
}
next();
});
这样我们就可以通过判断访问的域名是否在我们允许的域名列表中,如果存在就允许跨域访问,否则不允许。
Array.prototype.indexOf() 方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
关于 CORS 的知识可以看看这里。
CORS与JSONP比较
- 相比 JSONP 只能发 GET 请求,CORS 允许任何类型的请求。
- JSONP 的优势在于支持老式浏览器(IE浏览器不能低于IE10才能兼容 CORS),以及可以向不支持 CORS 的网站请求数据。
服务器代理实现跨域
因为浏览器端的同源策略才产生了跨域问题,所以我们可以使用服务器代理的方法绕开浏览器端:前端向与自己同源的服务器发起请求,该同源服务器再向不同源的服务器发送请求(请求转发),把同源服务器请求的数据再返回给前端就大功告成了。
首先看看端口号为3000的 node.js 服务器的代码:
/* GET home page. */
router.get('/', function(req, res, next) {
res.sendfile('./views/proxy.html');
});
router.get('/proxy', function(req, res, next) {
var headers = req.headers;
var options = {
host: 'localhost',
port: 5000,
path: '/query',
method: 'GET',
headers: headers
};
// 发起 HTTP 请求
var req = http.request(options, function(res) {
res.setEncoding('utf8');
res.on('data', function (data) {
//从端口号为5000的服务器中获取 data
var data = JSON.parse(data);
//获取数据后传回浏览器
success(data);
});
});
req.on('error', function(e){
console.log("problem with request:" + e.message);
});
req.end();
//获取数据后传回浏览器
function success(data){
res.send(data);
}
});
当开始服务器后访问http://localhost:3000/
,服务器传回一个proxy.html
文件给浏览器:
<!--proxy.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>proxy</title>
</head>
<body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script type="text/javascript">
$.ajax({
type:"get",
url:"http://localhost:3000/proxy",
dataType: "json",
success:function(data){
console.log(data);
}
});
</script>
</body>
</html>
proxy.html
网页中发起 ajax 请求向http://localhost:3000/proxy
获取数据,然后端口号为3000的服务器接受到请求后再发起 HTTP 请求获取http://localhost:5000/query
的数据。
显然,网页和端口号为3000的服务器就是同源的,这样子 ajax 请求肯定没问题,而且因为服务器端之间不存在跨域问题,所以端口号为3000的服务器向端口号为5000的服务器发送请求也没问题。
再来看看端口号为5000的 node.js 服务器的代码:
router.get('/query', function(req, res, next) {
var data = {
"name": "kaelyn"
}
res.json(data);
});
就这样,当我们启动两个服务器后,打开浏览器访问http://localhost:3000/
,就可以看到在控制台上打印出了返回的数据,这就是通过服务器代理解决跨域问题的方法。
关于 node.js 的 http.request 方法的知识可以看看这里。