两种跨域方案:CORS与JSONP

421 阅读7分钟

同源策略:

  • :我们可以在任意的一个网页的控制台输入window.origin,即可获取到当前网页的源。如:

image.png

:即**通讯协议(https)+域名(www.tencent.com)+端口号(默认为80端口)。**所以如果两个网页的协议、域名、端口号相同那么那么他们便是同源的。

  • **同源策略:如果一个js运行在源A里,那么只能获取当前源的数据,不能去获取其他源的数据。**举个例子:

如果我们在qq.com中引用了cdn.com/xx.js文件那么xx.js只能获取到当前源qq.com的数据,xx.js是运行在qq.com这个源里的,所以我们不能用其获取www.tencent.com源的数据。但这里需要注意的是我们引入js文件并不算是跨域访问,因为我们仅仅是获取js然后让他运行在我们的浏览器里并不是读取他们。

  • 为什么要设计同源策略

我们可以想一个简单的场景,如果我们登陆了我们的qq空间(使用Cookie),我们通过qzone.qq.com这个源发送Ajax请求获取到了这个源里面的数据,也就是我们的好友列表,这时其他人从另外的一个源假设是xxx.com也请求获取qzone.qq.com源下你好友列表的数据,此时服务器会响应这个请求并返回给其数据。原因是服务器并不能区分这两个请求,他们的区别仅仅是referer,如果服务器没有检查referer,这样我们的好友数据便会被偷取。所以总结一句话就是同源策略的作用是保护我们的隐私

模拟跨域访问:

接下来我们用实验来实验来验证一下浏览器阻止跨域访问的例子:

  • 首先我们通过node.js开通一个服务器,端口号为8888。

  • 在这个服务器中有三个文件,index.html,qq.js,friends.json,我们在index中引入我们的qq.js,在qq.js中发送Ajax请求来获取friends.json的数据并且在控制台输出他们。

qq.js的代码:

const request = new XMLHttpRequest()
request.open('GET', '/friends.json')
request.onreadystatechange = ()=>{
  if(request.readyState===4 && request.status === 200){
    console.log(request.response)
  }
}

request.send()

friends.json中的数据:

[  {"name":"吴彦祖"},  {"name":"周杰伦"}]

image.png

可以看到我们在控制台成功的访问到了当前源下的json数据。

  • 接下来我们再使用server创建一个服务器,指定其端口号为9999,这个服务器可以请求两个文件,index.html,与frank.js我们在frank.js发送Ajax请求去访问8888源下的friends.json中的数据。

frank.js文件:

function ajax(method, url) {
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest();
    request.open(method, url);
    request.onreadystatechange = () => {
      if (request.readyState === 4) {
        if (request.status === 200) {
          resolve(request.response);
        } else {
          reject(request);
        }
      }
    };
    request.send();
  });
}
ajax("get", "http://127.0.0.1:8888/friends.json").then(response => {
  console.log("这是 AJAX");
  console.log(response);
});

我们访问9999端口下的引入该js文件的index.html,发现无法得到8888端口下的friends.json中的数据,控制台会给我们报一个错:

image.png 我们从报错的信息可以看到,我们从http://127.0.0.1:9999这个源发送的Ajax请求http://127.0.0.1:8888/friends.json源下的数据被阻止了,原因是CORS policy(跨域资源共享策略)。

但这里我们需要注意的是我们的请求实际上是成功的,我们在8888的后台是监听到了friends.json被请求的,但是我们在9999端口下浏览器是不允许我们读取数据的。

总结一下就是浏览器会阻止我们对其他源数据的访问,但是我们是可以跨域使用像js,css,图片等数据的,原因是浏览器的同源策略只是阻止数据的访问,而不是阻止我们数据的使用,我们在通过其他源引入js,css,图片时只是在使用他们我们并不知道他们内部的数据是什么样的,也就是说我们可以通过像script标签来引用其他网页的js但我们根本不知道其内部的代码究竟是什么。如果我们想通过Ajax来请求访问其他源的js那么便会受到同源策略的限制。

方案一CORS:

既然两个不同源的数据是不允许被访问的,那么如果我有两个服务器非要彼此之间的数据可以互相访问有没有什么办法呢?我们便可以使用CORS方法:

CORS方法是解决跨域问题最简单的一种方法,既然我们想要获取别的服务器的同意,那么我们在我们需要访问资源的服务器上添加一个‘备注’,告诉这个服务器如果是这个源来请求数据,我们可以允许其访问数据。这样既保护了数据安全,又确保了我们跨域数据的访问。

// 在我们qq.com服务器的后端当访问friends.json文件时,添加一个响应头。
// 表明允许请求的url地址
response.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:9999");

image.png

这样我们再在我们9999端口的控制台看,发现之前的报错已经消失了我们成功使用Ajax请求到了数据。

方案二、JSONP:

CORS是一种非常方便的跨域解决方案,但是其唯一的缺点是IE6789都不支持,那么我们如何在不支持CORS的情况下跨域呢?于是便有了第二种方案JSONP

我们之前说过我们虽然不能使用Ajax来读取其他域名的数据但是我们可以使用script标签来引用其他域名的js呀,于是我们想到,我们使用script标签来获取js文件,在js文件中来夹带我们需要请求的数据,那么具体如何实现呢?

  • 首先我们在被请求的js文件中调用一个函数,在后端当我们发现这个js文件被请求时我们读取这个js文件,将函数调用时传递的参数替换为我们需要获取的数据(如json数据)。

  • 之后我们在需要请求数据的js文件中声明一个函数,我们使用script标签来请求数据得到一个函数的调用,函数调用时传递的实参,便是我们需要的数据,我们在声明函数时可以对其操作读取。

在被请求域名下的js文件中调用一个函数,在需要请求数据的js文件中声明一个函数,之后使用script标签来访问这个js文件便可以间接的得到我们需要跨域请求的数据啦。

那么这里还有一个问题,如何保证我们声明的函数与调用的函数名一样呢?我们是在window上声明的一个函数,如果要请求多个数据如何防止函数名重复呢?于是:

  • 我们在使用script标签请求数据的时候使用随机数来作为函数名,我们在使用script标签请求的时候给请求的url地址加上一个callback参数,在我们的后端读取这个参数代表的随机数,并且替换为调用的函数名。

这样既防止了函数名的冲突问题,也可以使我们声明的函数名与调用的函数名相同。

这里我们就完成了JSONP的基本功能了,但我们还可以进行优化,我们在使用script标签请求的时候我们可以将其封装为一个方法,使用Promise将其封装,将声明函数中得到的数据resolve出去。

发送请求的代码:

function jsonp(url) {
  return new Promise((resolve, reject) => {
    const random = "frankJSONPCallbackName" + Math.random(); // 生成函数名
    window[random] = data => {
      resolve(data);
    };
    const script = document.createElement("script");
    script.src = `${url}?callback=${random}`; // 传递callback参数
    script.onload = () => {
      script.remove(); // 最后删除我们引用的script标签,让html变的简洁
    };
    script.onerror = () => {
      reject();
    };
    document.body.appendChild(script);
  });
}

jsonp("http://127.0.0.1:8888/friends.js").then(data => {
  console.log(data);
});

监听friends.js文件的请求,并将调用函数的参数改为我们需要的data与替换函数名:

response.statusCode = 200;
response.setHeader("Content-Type", "text/javascript;charset=utf-8");
const string = `window['{{xxx}}']({{data}})`
const data = fs.readFileSync("./public/friends.json").toString();
const string2 = string.replace("{{data}}", data).replace('{{xxx}}', query.callback); 
response.write(string2);
response.end();

总结一下:

什么是JSONP?

  • 在不允许使用CORS情况下的一种解决方案,可以适配IE浏览器。
  • 具体过程就是我们使用script标签去请求一个js文件,js文件中调用一个函数,函数中有我们需要的数据。得到数据后在函数声明中使用数据。
  • 函数名为一个随机数,随机数是通过get请求传递给后端的,后端通过refer获取这个参数,并赋值给函数名。

JSONP的优点:

  • 相比于CORS其兼容IE浏览器。
  • 其可以跨域

JSONP的缺点:

  • 其是使用script标签访问的,所以其不能像Ajax那样获得精确的请求状态与响应头。
  • 由于其是script标签,所以其不能发送POST请求。