浏览器3:跨域、同源规避

453 阅读4分钟

一、跨域

为什么跨域?同源指的是:两个 url 的协议、域名和端口都相同,否则就是跨域。

// 协议是http,域名是www.example.com,端口是80(默认端口可以省略)
http://www.example.com/dir/page.html 

http://www.example.com/dir2/other.html  // 同源  
http://example.com/dir/other.html  // 不同源(域名不同)  
http://v2.www.example.com/dir/other.html  // 不同源(域名不同)  
  1. 服务端向服务端请求会产生跨域吗?

    跨域是对前端的浏览器做出的限制,服务器之间不存在跨域。

  2. 跨域请求会到达服务端吗?

    不论请求是否跨域,只要发出去了就一定会到达服务端并被执行。

  3. 跨域请求是谁在处理?

    服务端会处理跨域请求并返回响应,只不过响应被浏览器拦截了,浏览器隐藏了请求的返回值。

1. 同源策略的限制

跨域请求会带来一些安全问题,不法分子可能会利用它来发动 csrf 攻击。因此,浏览器采用同源策略对跨域请求作出限制,同源策略的限制主要体现为 3 个方面:

  1. 无法访问非同源网页的cookie,localStorage 和 IndexedDB。
  2. 无法读取和修改非同源网页的DOM。
  3. ajax请求只能发给同源网站。

二、规避同源策略

1. cookie

如果两个网页的一级域名相同,二级域名不同,那么可以通过设置相同的ducument.domain来共享cookie,这种方法只适用于 cookie 和 iframe 窗口

iframe 窗口:可以在当前网页嵌入其他网页。只有在同源情况下,父子窗口才能通信,可以拿到 DOM。

2. 跨文档通信 postMessage

对于完全不同源的网站,可以通过h5新增的API:postMessage() 来解决跨域窗口的通信问题,适用于localStorage、IndexedDB、iframe窗口。

我们假设父窗口 http://fawin.com,子窗口http://chwin.com  
var openWindow = window.open('http://chwin.com', 'title');  //父窗口打开一个子窗口
openWindow.postMessage('Welcome!', 'http://chwin.com');  //父窗口向子窗口发消息 

3. 对于ajax请求不能发送的情况

3.1 JSONP

jsonp 利用了<script>没有跨域限制的漏洞来获得数据,简单易用、老式浏览器全部支持、没有兼容性问题。它的做法是:

  1. 在网页中添加一个<script>标签,由它向跨域网页发出请求。请求的 url 中有一个 callback 参数 ?callback=bar,用来告诉服务器,客户端的回调函数名称是bar
<script src="http://api.foo.com?callback=bar"></script>
  1. 服务器收到请求后,会将相关的 json 数据放在回调函数名里拼成一个字符串,并返回bar({json数据})

  2. 客户端接收字符串后(script 标签请求的脚本内容会作为代码解析。因为已经定义了 bar 函数),就能在函数体内拿到返回的json数据

3.2 WebSocket

WebSocket 是一种 h5 的通信协议,使用 ws://(非加密)wss://(加密)作为协议前缀,该协议不实行同源政策

浏览器发出的 ws 请求的请求头中有一个Origin字段,表示该请求来自哪个域。服务器根据该字段判断是否许可本次通信,如果该域名在白名单中,服务器就会允许。

3.3 CORS (跨域资源共享):自动

CORS 请求是浏览器自动发起的请求,是解决跨域 ajax 请求的根本办法。CORS 请求分为简单请求复杂请求,简单请求需要同时满足两个条件:

  1. 请求方法get/head/post
  2. 限制请求的头信息不超出以下字段:
Accept // 通知服务器可以发回的数据类型
Accept-Language // 预期返回的人类语言
Content-Language // 希望采用的语言
Content-Type:只限于三个值`application/x-www-form-urlencoded``multipart/form-data``text/plain`

3.3.1 简单请求流程

浏览器会直接发出 CORS 请求,请求头中增加Origin字段说明该请求来自哪个源 (协议 + 域名 + 端口)

如果Origin不在服务端的许可范围内,那么服务端返回的响应中不包含Access-Control-Allow-Origin字段。反之就会多出几个头信息字段:

Access-Control-Allow-Origin: a.com  // 必须!白名单,表示哪些 url 可以访问该资源
Access-Control-Allow-Credentials: true  // 可选!是否允许跨域请求携带cookie
Access-Control-Expose-Headers: FooBar  // 可选!
Content-Type: text/html; charset=utf-8

CORS 请求默认不包含 cookie,如果跨域需要携带 cookie,那么分为3步

  1. 服务端在响应中需要设置Access-Control-Allow-Credentials: true
  2. 服务端的Access-Control-Allow-Origin不能使用通配符,必须设置成和请求网页一致的域名
Access-Control-Allow-Origin: a.com  // 表示信任 a.com,允许a.com 跨域请求
Access-Control-Allow-Credentials: true  
  1. 客户端必须在 ajax 请求中打开 withCredentials
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;  // 可以让跨域请求携带cookie

3.3.2 复杂请求流程

复杂请求会先进行一次预检请求,请求方法 OPTIONS,头信息会有两个特殊字段:

  • Access-Control-Request-Method:说明浏览器的CORS请求会用到哪些请求方法。
  • Access-Control-Request-Headers:说明浏览器CORS请求会额外发送的头部字段。

服务端接收到预检请求后会检查这些字段(包括Origin),检查通过后就会返回有Access-Control-Allow-Origin字段的响应。

预检请求通过后,之后的 CORS 请求就会和简单请求一样。

补充:复杂请求 = 预检请求 + CORS 简单请求,相当于给服务器一个提前拒绝的机会。

3.3.3 jsonp 和 cors 区别

  • 请求方式:get请求;所有类型。
  • jsonp 支持老式浏览器,不存在兼容性问题。