【前后分离】 跨域

443 阅读7分钟

完整代码请查看GitHub项目kuayu-1

同源策略--禁止跨域

  • 一个url的源 = 协议+域名+端口号
  • 在开发者工具、控制台里使用window.originlocation.origin可查看当前页面的源

image.png

同源

两个url的源必须一模一样,就说这两个url就是同源的

同源策略

  • 浏览器故意规定:如果一个JS文件运行在源A里,那么就只能获取到源A的数据。不能获取到其他源的数据,即不允许跨域
  • 不同源的页面之间不允许互相访问数据

举例(省略http协议)

  1. 假设frank.com/index.html这个页面引用了cdn.com/1.js这个js文件 那么就说 “1.js 运行在源frank.com里”
  2. 注意这跟cdn.com没有关系,虽然1.jscdn.com那下载,所以1.js就只能获取源frank.com的数据

浏览器这样做的目的--保护用户隐私!

如果没有同源策略

  1. 以QQ空间为例
  • 源为user.qzone.qq.com
  • 假设, 当前用户已经登录(用Cookie,后面会讲)
  • 假设,AJAX请求/friends.json可获取用户好友列表
  • 到目前为止都很正常
  1. 黑客来了
  • 假设你的女神分享qzone-qq.com给你
  • 实际上这是一个钓鱼网站
  • 你点开这个网页,这个网页也请求你的好友列表
  • 即用Ajax:请求的url为https://user.qzone.qq.com/friends.json
  • 请问,你的好友列表是不是就把黑客偷偷偷走了?

问题的根源

  1. 无法区分发送者
  • QQ空间页面里的JS和黑客网页里的JS
  • AJAX发的请求几乎没有区别(请求的url都是qq的friends.json)(只有请求头里的referfer有区别,一个是user.qzone.qq.com/index.html,…)
  • 如果后台开发者没有检查referer,那么就完全没区别
  • 所以,没有同源策略,任何页面都能偷QQ空间的数据,甚至支付宝余额!
  1. 那检查referer不就好了?
  • 安全原则:安全链条的强度取决于最弱一环
  • 万一这个网站的后端开发工程师是个傻呢
  • 所以浏览器应该主动预防这种偷数据的行为
  • 总之,浏览器为了用户隐私,设置了严格的同源策略

一些问题

  1. 为什么a.qq.com访问qq.com也算跨域?
  • 答:因为历史,上,出现过不同公司共用域名, a.qq.com和qq.com不一定是同一个网站,浏览器谨慎 起见,认为这是不同的源
  1. 为什么不同端口也算跨域?
  • 答:原因同上,一个端口一个公司。记住安全链条的强 度取决于最弱一环,任何安全相关的问题都要谨慎对待
  1. 为什么两个网站的IP是样的,也算跨域?
  • 答:原因同上,IP可以共用。
  1. 为什么可以跨域使用CSS、JS和图片等?
  • 答:同源策略限制的是数据访问,我们(在HTML里)引用CSS、JS 和图片的时候,其实并不知道其内容,我们只是在引用执行。不能读取内容(数据)(在Ajax里),因为不是同源 不信我问你,你能知道CSS的第一个字符是什么吗?

我没搞懂:意思是Ajax里的所有请求不可以跨域,但是Html里的script标签、link标签也就是浏览器去请求就可以引用其他域的js文件,css文件。Html里的script标签、link标签是发js、css请求,也就说在html里面的js、css请求可以跨域。但是我也能看见js、css的内容啊

演示

image.png

第一步:初始化

1. 创建目录qq.com(模拟QQ网站)和yy.com(模拟黑客网站)

2. 在文件夹qq.com里

  • 把文件夹qq.com拉入VSCode

  • 创建文件server.js,作为QQ的服务器,模拟QQ网站。

  • 新建public/index.html,并且把它的内容 作为 请求/index.html(首页)的响应体

image.png

  • 新建public/qq.js,并且把他的内容 作为 请求/qq.js(JS脚本文件)的响应体

  • 新建public/friends.json,并且把他的内容 作为 请求/friends.json的响应体

image.png

  • 打开终端,node-dev server.js 8888启动服务器

  • 也就是说,在用一个服务器里的请求都是同源的,/index.html和/qq.js和/friends.json都是同源的

3. 在文件夹yy.com里

  • 文件 → 将文件夹添加到工作区 → 选择yy.com

  • 创建文件server.js,作为黑客网站的服务器,模拟黑客网站。

  • 新建public/index.html,并且把它的内容 作为 请求/index.html(首页)的响应体

image.png

  • 新建public/qq.js,并且把它的内容 作为 请求/qq.js(JS脚本文件)的响应体

  • 打开终端,node-dev server.js 9990启动服务器

  1. server.js的代码请看github项目

image.png

第二步:显然,QQ空间和黑客网站是不同源的,因为端口号不一致

image.png

image.png

第三步:开始用Ajax模拟同源策略

1. 让QQ空间的js去请求friends.json,请求成功并且得到响应

  • 在qq.js里请求friends.json
const request = new XMLHttpRequest();
request.open("GET", "/friends.json");
request.onreadystatechange = () => {
  if (request.readyState === 4) {
    if (request.status >= 200 && request.status < 300) {
      console.log(request.response);  //请求成功,就打印出响应体的内容,也就是好友列表
    }
  }
};
request.send();

  • 请求成功,得到响应

image.png

  • 因为QQ空间引用了/qq.js,所以qq.js运行在源"http://localhost:8888"里,所以qq.js可以请求也在源"http://localhost:8888"里的/friends.json

2. 让黑客网站的JS去请求friend.json(偷数据),请求失败

  • 在yy.js里请求friends.json,注意请求的url要写完整,不写完整你咋知道你要偷的是哪个源的friends.json的数据
const request = new XMLHttpRequest();
request.open("GET", "http://localhost:8888/friends.json");
request.onreadystatechange = () => {
  if (request.readyState === 4) {
    if (request.status >= 200 && request.status < 300) {
      console.log(request.response);
    }
  }
};
request.send();

  • 请求失败

因为黑客网站引用了/yy.js,所以/yy.js运行在源"http://localhost:9990"里,所以yy.js可以无法请求源"http://localhost:8888"里的/friends.json

image.png

image.png

CORS--我就要跨域

简介

CORS,全称Cross-Origin Resource Sharing ,是一种允许当前域(domain)的资源(比如html/js/web service)被其他域(domain)的脚本请求访问的机制,通常由于同域安全策略(the same-origin security policy)浏览器会禁止这种跨域请求。

做法

  • 我是源"http://localhost:8888"的老板,我同意我的/friends.json可以给运行在源"http://localhost:9990"的JS请求并且获取到响应!

  • 那我就要在我的服务器里的/friends.json路由里面设置响应头,允许9990源请求本地址 response.setHeader('Access-Control-Allow-Origin',"http://localhost:9990")

  • 完事!!!

image.png

详细做法

当你工作中需要用到时,请看MDN文档

JSONP--IE就要跨域

  • 虽然不允许跨域,但这仅限于数据内容,我们还是可以引用任何网站的js的
  • 因此,我是源"http://localhost:8888"的老板,我同意我的/friends.json里的数据内容可以源"http://localhost:9990"
  • 那么我就让我的小员工把/friends.json的数据内容,写到/friends.js里面
  • 这样源"http://localhost:9990"(还有任何源)的html里引用/friends.js了,他想要的内容也在里头了

步骤

1. qq.com将数据写到/friends.js

  • 新建public/friends.js,里面写
window.xxx = {{data}}  //{{data}}是占位符  

//另一种方法:先打电话给黑客网站,你先在你的js脚本文件里写个window.xxx函数,这个函数会把参数打印出来,所以我这边就把你要的数据当成window.xxx函数的参数
widow.xxx({{data}})

  • 在服务器中新增加路由 /friends.js

把public/friends.js的占位符替换成public/friends.json的数据内容,最后 响应体内容就为替换后的public/friends.js的内容

else if (path === "/friends.js") {
    response.statusCode = 200;
    response.setHeader("Content-Type", "text/javascript;charset=utf-8");
    const string = fs.readFileSync("./public/friends.json").toString();
    const data = fs.readFileSync("./public/friends.json").toString();
    const content = string.replace("{{data}}", data);
    response.write(content);
    response.end();

2. 黑客网站完全可以引用qq网站的js

  • 需要给黑客网站的html里面加入script才可以引用QQ网站的/friends.js,那就用DOM操作,在黑客网站的JS脚本文件里面用DOM操作
  • 在yy.js里
const script = document.createElement("script");
script.src = "http://localhost:8888/friends.js";
document.body.appendChild(script);


  • 此时,黑客网站里面就引用了/friends.js了,就会按照/friends.js里面的响应体js代码执行:加一个全局变量xxx,变量值为好友列表!
window.xxx = [{ "name": "Linda" }, { "name": "Jack" }, { "name": "Pike" }];


//另一种方法
window.xxx ( [{ "name": "Linda" }, { "name": "Jack" }, { "name": "Pike" }]);


  • 然后,我们可以把好友列表打印出来,那就是要在黑客网站自己的js脚本文件/yy.js里操作。
script.onload = () => {
  console.log(window.xxx);
};  //监听script标签的加载完成事件,当script标签生成完毕时,就执行函数打印出window.xxx


//另一种方法:我接到qq的电话,所以我要写个window.xxx函数
window.xxx = x=>{console.log(x)}
//这样当我一把friends.js引用过来,内容是window.xxx ( [{ "name": "Linda" }, { "name": "Jack" }, { "name": "Pike" }]) ,我也正好有这个函数,这样好友列表就打印出来了


image.png

image.png

image.png

image.png

注意:window.xxx就是一个回调啊!跨域名回调!yy.js定义了这个函数,当friends.js调用这个函数

注意:不一定数据来源于json,完全也可以是xml啊,所以JSONP这个名字真的和JSON没有什么关系

优化一:让写成js的数据定向分享,而不是哪个源都可以引用

检查请求头里的referer,对服务器的路由/firends.js进行改写

 else if (path === "/friends.js") {
    if (request.headers["referer"].indexOf("http://localhost:9990") === 0) {
      response.statusCode = 200;
      response.setHeader("Content-Type", "text/javascript;charset=utf-8");
      const string = fs.readFileSync("./public/friends.js").toString();
      const data = fs.readFileSync("./public/friends.json").toString();
      const content = string.replace("{{data}}", data);
      response.write(content);
      response.end();
    } else {
      response.statusCode = 404;
      response.end();
    }

优化二:万一window.xxx这个函数已经被用了,那就不用window.xxx,用window.随机数,这个总不会有人和我重复了吧

  1. 在黑客网站的JS脚本文件yy.com
  • 用随机数给函数取名
  • 巧用查询参数引入随机数函数名(其实大家不用functionName,而是Callback)
const random = `yyJSONPCallbackName` + Math.random();
window[random] = x => {
  console.log(x);
};

const script = document.createElement("script");
script.src = `http://localhost:8888/friends.js?functionName=${random}`;
document.body.appendChild(script);


  1. 在qq.com的/friend.js
  • 把函数名先用占位符占起来
window[`{{xxx}}`] ({{data}});

  1. 在qq.com的服务器server.js的分享数据的js路由/friends.js
  • 获取到请求的请求头的查询参数的functionName,这将是函数名
  • 所以将friends.js里的占位符替换成函数名就行了。
else if (path === "/friends.js") {
    if (request.headers["referer"].indexOf("http://localhost:9990") === 0) {
      response.statusCode = 200;
      response.setHeader("Content-Type", "text/javascript;charset=utf-8");
      const string = fs.readFileSync("./public/friends.js").toString();
      const data = fs.readFileSync("./public/friends.json").toString();
      const content = string
        .replace("{{data}}", data)
        .replace("{{xxx}}", query.functionName);  //函数名的占位符替换成我们事先存在查询参数里的随机数
      response.write(content);
      response.end();
    } else {
      response.statusCode = 404;
      response.end();
    }
  }

优化三:黑客网站每次引用qq.com的js(每次发JSONP请求),都会在自己的html里面加一个script标签,好臃肿!

  1. 在黑客网站的JS脚本文件yy.com
  • 当script标签加载好就删除
const random = `yyJSONPCallbackName` + Math.random();
window[random] = x => {
  console.log(x);
};

const script = document.createElement("script");
script.src = `http://localhost:8888/friends.js?functionName=${random}`;
script.onload = () => {
  script.remove();
};
document.body.appendChild(script);

优化四 :qq.com的后台专门用来存给别人分享数据的JS文件friends.js,就tm一行,你还在路由中引用了这个文件,这不是没事找事

  • 删除qq.com的friend.js
  • 把他的内容直接放入服务器/friends.js的路由中去
 else if (path === "/friends.js") {
    if (request.headers["referer"].indexOf("http://localhost:9990") === 0) {
      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 content = string
        .replace("{{data}}", data)
        .replace("{{xxx}}", query.functionName);
      response.write(content);
      response.end();
    } else {
      response.statusCode = 404;
      response.end();
    }
  } 

用promise封装JSONP

  • 只需要给一个url,就可以执行发JSONP请求
  • 在黑客网站的JS脚本文件yy.js
//封装JSONP
function jsonp(url) {
  return new Promise((resolve, reject) => {
    const random = `yyJSONPCallbackName` + Math.random();
    window[random] = x => {
      resolve(x); //成功就执行resolve函数
    };
    const script = document.createElement("script");
    script.src = `${url}?functionName=${random}`;
    script.onload = () => {
      script.remove();
    };
    script.onerror = () => {
      reject();
    }; //失败就执行reject函数
    document.body.appendChild(script);
  });
}

//调用jsonp函数
jsonp("http://localhost:8888/friends.js").then(x => {
  console.log(x);
});

最后:什么是JSONP

1、先回答JSONP是啥

  我们在跨域的时候,由于当前浏览器(可恶的IE)不支持CROS,或者因为某些原因不支持CROS,那我们就必须用JSONP来跨域。于是请求一个JS文件,这个JS文件会执行一个回调,回调里面就有我们的数据。   我们当前网站创建一个script标签去请求另外一个网站的js,js里面会夹带我要的数据,而且这个js会调用我写的一个全局函数来运行,就可以把数据给我了。       这个回调的名字是我们随机生成的随机数,我们把这个回调的名字以callback的参数传给后台,后台就会把这个函数返回给我们,在执行

2、优点

  • 兼容IE
  • 跨域(本来目的就是跨域,我笑了)

3、缺点

  • 因为script标签引用,得不到详细信息:状态码、header
  • 因为script标签引用,只可以发get请求,当然不能支持POST

image.png

image.png

image.png