浏览器的同源策略、跨域限制

1,274 阅读10分钟

跨域

跨域:web中,文件是由服务器提供的,这就决定文件本身有一个 【主机、端口、协议】的概念。两个文件不位于同一个域但是存在交互就称之为“跨域”

IE 未将端口号纳入到同源策略的检查中

postman和客户端APP里面,每发出一个请求,都是在独立请求一个资源,而不是在一个网站返回的页面里,再去请求另外一个网站/端口的资源。自然也就不会造成跨域了。

为什么要跨域?-因为不会把所有资源放在一台服务器上,这种情况常用修改域名解决

实际资源交互发生的场景不一,有iframe同源限制、http请求限制等解决跨域的思路:

  1. 伪造同源条件
  2. 交给有能力跨域的代理,如服务器
  3. 使用浏览器不限制跨域的请求方式 JSONP、postMessage
  4. 使用跨域的资源共享的数据体
  5. 前后端配合,协调协议,e.g:http协议头部沟通允许跨域

浏览器"同源策略":http跨域限制、Cookie跨域限制、iframe跨域限制、 LocalStorage跨域限制

iframe显示的文档与顶级文档不同源时候,同源限制 相关的安全问题,推荐查看另一篇博客

代理服务器

跨域访问其实只是浏览器的同源策略,服务器没有这个限制

  • Node.js的http模块等都可以抓取其他网站的页面。
  • 把前端Ajax需要跨域访问的地址交给后台服务器

Nginx http代理服务、反向代理服务器(HTTP中间件)

  • 正向代理:代理客户端发起请求。对客户端已知,对服务端透明(服务器不知道谁在访问)的代理应用,称为正向代理。如:梯子
  • 反向代理:代理服务器,服务器内置几个服务器,用户去访问。

nginx反向代理处理请求实现跨域 解决前后端分离开发中,前端对接口联调中的跨域问题:通过Nginx反向代理将对真实服务器的请求转移到本机服务器来避免浏览器的"同源策略限制"

将nginx反向代理服务器部署在浏览器同源位置,在配置文件nginx.conf 中设置端口监听转发请求

vue-cli中的devServer和webpack dev server里面changeOrigin解决跨域实质上也是服务器代理,就类似Nginx反向代理这样

JSONP JSON with padding

现在基本不采用

< script >标签的src属性并不被同源策略所约束,可以获取任何服务器上脚本并执行

原理:允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问。相比img的图片探测,它可以实现双向通信。 本质是发起一个HTTP的GET请求

<script src="http://dohpi?param1=a&param2=b&callback=jsonp"></script>
<script>
    function jsonp(data) {
    	console.log(data)
	}
</script>

// jQuery也支持JSONP方法
// jQuery.getJSON()
$.getJSON(url,[data],[callback])
// 也可在url中指定回调函数的名字
script.src = 'http://free.net/json/?callback=handleRequest'
  • 不安全:容易造成xss攻击(去请求别的域,得到别的域返回的数据,万一是个脚本,那就注入到自己的代码里了)
  • 污染命名空间:加载的js相比模块js没有单独的全局命名空间,和当前js公用一个全局命名空间

CORS

需要浏览器和后端同时支持

  • 后端:如果服务器支持CORS,设置Access-Control-Allow-Origin配置可访问的域名
  • 前端:浏览器检测到相应的设置,会自动进行对应的 CORS 通信

浏览器判断是否跨域,所有跨域的js在提交请求的时候,???浏览器怎么知道??如果服务端设置了CORS,需要分简单请求(simple request)和非简单请求(not-so-simple request)来处理。

简单请求:

  1. HEAD
  2. GET
  3. 部分POST(header:application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一种)

复杂请求:

  • 浏览器首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。
  • 服务器确认允许之后,才发起实际的 HTTP 请求。
  • 在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

预检请求(preflight request): 使用OPTIONS方法发送并包含以下头部

  1. Origin:请求端申明自己的源
  2. Access-Control-Request-Method:
  3. Access-Control-Request-Header:

S端判断是否允许这个域访问,自动添加跨域请求配置的头信息(如下)

  1. Access-Control-Allow-Origin
  2. Access-Control-Allow-Credentials
  3. Access-Control-Expose-Headers
  4. Access-Control-Max-Age
  5. Access-Control-Allow-Methods
  6. Access-Control-Allow-Headersdocument.domain

document.domain

只能用于主域相同的域之间的数据通信【必须属于同一个基础域名,协议、端口都要相同】

当主域之间相互通信的时候,只要将两者的document.domain赋值为当前的主域地址,即可实现跨域通信。document.domain可以扩大范围,扩值不可逆

e.g:
a.second.com:3000/a.html
b.second.com:3000/b.html document.domain = 'second.com' 表示二级域名都相同就可以实现跨域

实时双向Web技术

HTML5提出了 WebSocket库,同时Node.js后端使用ws模块支持。使用前需要确保浏览器是否支持。 HTTP是单向通信,只有客户端请求服务器端响应,但是websocket是双向的,双方都可以主动向对方发消息。 websocket需要HTTP协议来完成握手操作,握手结束之后,双方使用websocket协议通信,与HTTP再无关系。

postMessage(H5)

过去:跨源或者窗口间通信需要通过服务器

window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,就可以安全地实现跨源通信,即不同执行上下文之间(不同工作线程、不同源的页面)。常用来获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

// 发送消息端
otherWindow.postMessage(message, targetOrigin, [transfer]);

/*
otherWindow即其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
message将要发送到其他 window的数据,会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。
targetOrigin 通过窗口的origin属性来指定哪些窗口能接收到message消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。
*/


// 接收消息端
var mc = new MessageChannel()
mc.addEventListener('message', event => {
  var origin = event.origin || event.originalEvent.origin
  if (origin === 'http://test.com') {
    console.log('验证通过')
  }
})

在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。

网络请求

实质:浏览器与服务器的通讯

  1. Java小程序、flash影片
  2. 远程脚本、内嵌窗口
  3. Ajax 异步JavaScript+XML的技术概念:请求额外数据而不刷新页面
  4. XHR对象
  5. Fetch API

XMLHttpRequest对象 同/异 步

XMLHttpRequest对象 fetch API

默认情况下,XHR只允许访问 与发起请求的页面在同一个域的资源。想要实现跨域XHR,从实现跨域HTTP入手,CORS方法、CSP头部。此外也可以直接不用XHR,采用其他不受跨域限制的方法,jsonp、img图片探测

一个异/同步(配置open方法的参数来实现同/异 步)调用对象,设置响应HTTP请求状态变化的函数

let xhr=new  XMLHttpRequest();

//收到响应后,xhr的部分属性会被填充赋值
xhr.onreadystatechange=function(){
	if(xhr.readyStatus==4){
		if(xhr.status>=200&&xhr.status<300||xhr.status==304){
			alert(xhr.responseText);
		}else{
			alert("Request failed :"+xhr.status);
		}//不用this取代xhr,防止作用域问题
	}
};

xhr.open("get","./url",请求是否异步的布尔值);//请求方法,不请求,只是做准备,只能访问同源URL
//修改添加 应在这时完成
xhr.send(请求体);//无请求体,传入null

一般浏览器自己的默认HTTP消息头都会携带上去,此外 open()与send()函数之间开发者可以使用setRequestHeader()发送自定义的头部

属性

  1. responseText:作为响应体返回的文本
  2. responseXML:当响应体是XML时
  3. status:响应的HTTP状态
  4. statusText:响应的HTTP状态描述
  5. readyState:当前处在请求/响应过程的哪一阶段0-4,会触发readystatechange事件,为了保证跨流域兼容,onreadystatechange事件处理程序应该在调用open()之前赋值。
  6. timeout:毫秒数,不响应触发timeout事件,超时之后访问state会报错,在onreadyState写好catch/try

还有其他进度事件:loadstars、progress、error、

readyState

status是服务器对请求的反馈,而readystate表明客户端的交互状态过程。 ajax.readyState由AJAX对象与服务器交互时所得。无论访问结果如何,运行ajax都一定经历过的状态,即AJAX运行步骤。 (由数字1~4单位数字组成)

0 - (未初始化)还没有调用send()方法 1 - (载入)已调用send()方法,正在发送请求 2 - (载入完成)send()方法执行完成, 3 - (交互)正在解析响应内容 4 - (完成)响应内容解析完成,可以在客户端调用了

监听readyState可以实现解析完成立即调用

get请求

在get请求后添加的查询字符串参数,必须正确编码添加到URL后,然后再传给open() encodeURLComponent编码,&分割所有名/值对

可以自定义一个addURLParam()来保证编码正确

function addURLParam(url,name,value){
	url+=(url.indexOf("?")==-1? "?":"&");
	url+=endodeURIComponent(name)+"="+endodeURIComponent(value);
	return url;
}

post请求

相比get请求占更多资源,速度慢 向服务器发送应该保存的数据,应该再请求体中携带提交的数据,数据可以是任意格式

默认,服务器而言POST与提交表单不一样。提交表单可以通过添加头部Content-Type来告诉服务器,再通过调用serialize函数序列化表单数据之后作为请求体。除此之外,XHR 2新增了FormData类,保存表单数据,直接可以作为请求体,无需添加特殊头部.

const form = document.getElementById('iii');
xhr.send(serialize(form));
xhr.send(new FormData(form));
// 也可以这样添加数据
FormData.append('key','value')

Fetch API 异步

Fetch API 教程 - 阮一峰的网络日志 (ruanyifeng.com)

支持promise、service worker 能够执行XHR对象的所有任务,但更容易使用,是使用js请求资源的工具。 fetch方法给指定URL发送请求,是暴露在全局作用域中,包括主页面执行线程、模块和工作线程。

let r=fetch('/资源的url');//返回一个promise
r.then( (response) => {
	response.text
})

获取二进制数据

通常后端返回给我们的数据格式是json,除此之外,还有其他的格式

过去获取二进制数据,需要通过XMLHttpRequest对象的overrideMimeType方法来重载所获取数据的MIME类型,将所获取数据的字符编码charset修改为用户自定义类型。 HML5的XMLHttpRequest对象新增了responseType(属性值见下) 与response属性

arraybuffer:response 是一个包含二进制数据的 JavaScript ArrayBuffer。 blob:response 是一个包含二进制数据的 Blob 对象 。 document:response 是一个 HTML Document 或 XML XMLDocument,这取决于接收到的数据的 MIME 类型。 json:response 是一个 JavaScript 对象。这个对象是通过将接收到的数据类型视为 JSON 解析得到的。 text:response 是一个以 DOMString 对象表示的文本。

Beacon API

是一个基于JavaScript的Web api,一种非常有用的记录运动方法,可以将数据从页面发送回服务器,尤其是在日志记录环境中,浏览器支持非常广泛,能够无缝地记录数据,而不影响用户体验和网页性能,请求的非阻塞性质意味着性能比XHR和Fetch等替代方案快得多。

信息少量而快速

  • 类似unload这样的情况下成功发送请求,同时不影响下一个页面的载入
  • 任何时候都可以调用,浏览器会把请求添加到一个内部的请求队列,浏览器会主动地发送队列中的请求。
  • 浏览器保证在原始页面已经关闭的情况下也会发送请求。
  • 给navigator添加了一个sendBeacon()方法
navigator.sendBeacon(‘url',数据的有效负载参数);

web Socket

  • 得到了所有主流浏览器的支持
  • 是与服务器的全双工、双向通信渠道,不使用HTTP,而是自定义协议。目的是更快地发送小数据块。这需要支持该协议的专用的服务器,但速度优势明显。
  • 使用JavaScript创建web socket时,一个Http请求会发送到服务器以初始化连接,服务器响应后,使用HTTP的upgrade头部从HTTP协议切换到其他协议。
let socket=new WebSocket("ws://www.example.com/server.php");//不限制同源
socket.send("hihi");
//服务器回答消息,会触发message消息,可通过event.data访问到消息