Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

跨域不完全探究 #37

Open
MrErHu opened this issue Dec 2, 2018 · 0 comments
Open

跨域不完全探究 #37

MrErHu opened this issue Dec 2, 2018 · 0 comments
Labels
base Front End Basis

Comments

@MrErHu
Copy link
Owner

MrErHu commented Dec 2, 2018

跨域不完全探究

前言

  首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励。最近工作上事情比较繁多导致博客一度断更,为了挽救惨淡的关注量并兼顾自己有限的时间,准备近期多推出一些有关前端的基础知识学习,一起夯实基础。希望大家多多关注呀!
  

引子  

  跨域问题在Web开发中一直都是一个非常常见的问题,但在日常工作说实话使用频率并没有那么多,再加上重使用轻原理导致我对跨域问题理解的一直非常的浅显,借此机会让我们好好探索一下。

同源策略

  跨域问题的起源于众所周知的浏览器同源策略。作为如今Web安全基石的同源策略,早在上个世纪九十年代由网景公司提出,当时仅仅只是针对于Cookie的访问,即不同源的网页之间Cookie不能共享。后来同源策略变得更加严格,安全性也逐步提高,升级为:

  1. 不同源域 Cookie、LocalStorage 和 IndexDB 无法读取。
  2. 不同源域 DOM 无法获得。
  3. 不同源域 AJAX 请求不能发送

  说了这么多同源,那么何为同源。通常情况下,如果一个源拥有相同的协议(protocol)、主机(host)、端口(port),则称其为同源。当然我们说了通常情况下,也就意味着这个方面有例外存在,IE毫无疑问的在这个方面承担了它固有的职责。IE并没有将端口号加入到同源策略的组成部分,因此http://host:81http://host:80是属于同源的。虽然同源的安全限制是必要的,但是同时也带了束缚,假设我确实需要向不同源的地址发送请求该怎么办呢?那就回到了我们所要讨论的跨域问题。

跨域解决方案

JSONP

  每每提起跨域问题,第一个想到的就是JSONP了。JSONP与JSON虽然看起来很像,但却有本质区别
。JSONP全称是JSON with Padding,并不是一种新的数据格式,而是数据格式JSON的一种“使用模式”。浏览器的同源限制对含有src属性的元素(例如: scriptimg)不起作用,JSONP就是利用了script的这个特性。

基本原理

  前面我们说了JSONP利用的是script标签,绕过浏览器的同源策略,试想我们通过一个URL获取JSON数据是可能的。然而JSON数据却是不可执行的,因此JSONP通过将返回的JSON数据用函数包裹来实现执行的目的,这也就是JSONP中P表示padding(包裹)的含义。因为需要函数包裹数据,所以前后端需要提前约定好函数名,我们一般会在请求的URL中带上函数名称作为参数。例如请求接口:

http://api.demo.com/data?userid=1&jsonp=parseResponse

  服务器会将对应的数据填充到函数parseResponse:

parseResponse({"Name": "小明", "Id" : 1823, "Rank": 7})

概念实现

  我们实现一个最简单的函数用来创建一个JSONP请求:

function createJSONPRequest(url, callbackName){
    var script = document.createElement("script");
    script.src = url +"?callback=handleResponse"; 
    document.body.appendChild(script);
}

function handleResponse(res){
    console.log(res);
}

createJSONPRequest("http://localhost:3001/jsonp", "parseResponse")

然后我们用express.js来响应这个JSONP请求:

// 服务器端口号:3001
app.get('/jsonp', (req, res) => {
	var callbackName = req.query.callback;
	var mockData = {
	  name: "MrErHu"
	};
	res.send(`${callbackName}(${JSON.stringify(mockData)})`);
})

运行时你会发现浏览器会正确打印出跨域访问的数据,说明我们的跨域请求成功。

实践

  当然我们并不需要在前端手动去实现该函数,很多第三方库已经封装了JSONP的请求,我们以JQuery提供的Ajax理由为例,上面的请求用JQuery去实现代码是:

// 客户端端口号:3000
$.ajax({
    url: "http://localhost:3001/jsonp",
    type: "GET",
    dataType: "jsonp",
    jsonpCallback: "parseResponse",
    success: function (res) {
        console.log(res);
    }
});

  我们可以看到在JQuery中提供的ajax函数中通过配置dataTypejsonp,则可以实现JSONP请求,需要注意的是JQuery只是将其封装在ajax函数内,二者实质上有本质区别。

  了解JSONP的实现原理,我们知道这玩意肯定不会存在浏览器兼容性问题,毕竟我不相信会存在不支持script标签的浏览器。但是因为正是通过script标签实现的,JSONP也就只能支持GET请求,那么如果我们想实现POST请求的跨域有什么好的办法吗?

CORS

  CORS是Cross-origin resource sharing的缩写,即跨域资源共享,属于W3C标准,允许跨域发送XMLHttpRequest请求,支持多种HTTP Method。与JSONP的原理不同的是,CORS采用的是前后端HTTP表头协商的方式(我自己起的名字)判断是否允许跨域访问,并且相比与JSONP来说,CORS需要客户端和服务端共同支持,并且整个过程由浏览器自动完成。对于前端开发者而言,跨域的CORS通信与普通的Ajax通信没有差别,整个跨域处理的过程主要集中在服务器端。CORS通信分成简单请求(simple request)和非简单请求(not-so-simple request)两种模式。

简单请求

  所谓的简单请求是指,请求方法属于: HEADGETPOST之一,并且HTTP的头信息不超出以下几种字段:

Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:仅限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain

  如果不满足以上任一条件,均属于非简单请求。对于简单请求,浏览器会直接发送CORS请求。当我们使用Ajax发送一条跨域请求,浏览器会在HTTP请求头(Request Headers)中增加Origin属性:

  Origin属性用来表明当前的请求来自于哪个源(协议、主机、端口)。如果服务端支持CORS跨域,则需要在响应头中返回以下属性:

Access-Control-Allow-Origin: 
Access-Control-Allow-Credentials
Access-Control-Expose-Headers
  • Access-Control-Allow-Origin: 表示跨域的访问的源,其中"*"表示允许来自任何源的请求跨域访问
  • Access-Control-Allow-Credentials: 表示跨域访问是否允许带有Cookie信息,该值仅可以允许设置为true。如果服务器不允许请求携带Cookie,则不需要发送该属性。值得注意的是,携带的Cookie信息仅仅只是所跨域的服务器域名设置的Cookie,不能携带其他域名下的Cookie信息,Cookie仍然循序同源策略。并且还需要满足Access-Control-Allow-Origin指定的域与当前的请求的域完全一致(不能是"*")以及在XMLHttpRequest显式设置withCredentials属性为true,表示浏览器也允许发送Cookie。
  • Access-Control-Expose-Headers: 该字段可选,表示浏览器可以从该XMLHttpRequest请求中拿到的响应头信息。默认仅能拿到Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma这六个属性,如果想取得其他的属性,必须在该属性中指定。

我们用express.js模拟一下:

// 服务器端口号:3001
app.post('/cors', (req, res) => {
	var mockData = {
		name: "MrErHu"
	};
	var origin = req.get("origin");
	res.set("Access-Control-Allow-Origin", origin);
	res.set("Access-Control-Allow-Credentials", true);
	res.send(JSON.stringify(mockData))
});

  当我们前端Ajax请求:

// 浏览器端口号:3000
$.ajax({
    url: "http://localhost:3001/cors",
    type: "POST",
    success: function (res) {
        console.log(res);
    }
});

你会发现该条请求已经不会出现跨域问题,可以正确访问到数据。

非简单请求

  对于非简单请求,浏览器会预先发送一次预检请求,询问服务器是否允许跨域访问以及是否允许HTTP方法,只有得到肯定答复后,浏览器才会发出正式的HTTP请求。比如我们跨域向服务器发送一次PUT请求:

// 浏览器端口号:3000
$.ajax({
    url: "http://localhost:3001/cors",
    type: "PUT",
    success: function (res) {
        console.log(res);
    }
});

  你会发现浏览器首先会发送OPTIONS请求去预检本次跨域请求。

  当预检请求检测了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers属性后就会正式发送请求。需要注意的是,不仅仅是上述三个属性,预检请求还会返回Access-Control-Max-Age属性用来表示该条预检请求的有效期,在有效期内,浏览器不会再次发送预检请求,仅会使用该条缓存的预检请求。

  我们用express.js来模拟本次请求:

// 服务器端口号:3001
app.options('/cors', (req, res) => {
	var origin = req.get("origin");
	res.set("Access-Control-Allow-Origin", origin);
	res.set("Access-Control-Allow-Credentials", true);
	res.set("Access-Control-Allow-Methods", "PUT");
	res.set("Access-Control-Max-Age", 24 * 60 * 60 * 1000);
	res.send();
});

app.put('/cors', (req, res) => {
	var mockData = {
		name: "MrErHu"
	};
	var origin = req.get("origin");
	res.set("Access-Control-Allow-Origin", origin);
	res.set("Access-Control-Allow-Credentials", true);
	res.send(JSON.stringify(mockData))
});

  你就会发现本次PUT跨域请求成功,正确返回数据,达到了我们跨域的要求。

对比

  与JSONP相比,CORS支持更多的HTTP方法并且整个过程由浏览器自动完成,但是仅仅只兼容IE10及以上的浏览器,如果需要兼容老版本IE浏览器,CORS可能就不适合你了。
  

后言

我们这边文章着重讲述了两种跨域的基本原理,但实际上围绕跨域问题还有很多值得学习的地方,比如后端对跨域请求的处理,以及前端跨域带来种种的安全性问题。其实不仅仅是JSONP与CORS,Websocket也能实现跨域请求,以后有机会可以学习一下,最后还是欢迎大家关注我的博客,水平欠佳,希望体谅,愿一同进步。

@MrErHu MrErHu added JavaScript base Front End Basis and removed JavaScript labels Dec 2, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
base Front End Basis
Projects
None yet
Development

No branches or pull requests

1 participant