学习笔记 - Web开发 - 理解及实现跨域

理解及实现跨域

最近面试时,技术总监提出的这个问题,一时间不知从何说起,在这里写上一些自己理解和资料笔记,以便后日整理和修改。

在谈正题之前,先提一提与其相关的认知,有助于深入理解:

  1. 域是什么
  2. 跨域又是什么
  3. 跨域的原因,即为什么需要跨域
  4. 跨域有哪些方式
  5. 跨域的原理

1. 基本概念

要说跨域,那么不得不先谈谈域是什么,不然从何谈起跨域。

域也就是”同源策略“中的”源”、”数据来源”,而所谓同源,或者说一个域,即是”三个相同”:

协议相同
域名相同
端口相同

对于绝对的URIs,一个”源”(或者”域”)就是{协议,主机,端口}定义的。只有这些值完全一样才认为两个资源是同源的(“同一个域内的”)。

那么不同的域可以概括为:

只要协议、域名、端口有任何一个不同,都被当作是不同的域。

举例来说,http://www.example.com/dir/page.html 这个网址,协议是http://,域名是www.example.com,端口是80(默认端口可以省略)。它的同源情况如下:

http://www.example.com/dir2/other.html:同源
http://example.com/dir/other.html:不同源(域名不同)
http://v2.www.example.com/dir/other.html:不同源(域名不同)
http://www.example.com:81/dir/other.html:不同源(端口不同)

说到这里还未理解一个域(源)的含义,可以参考以下资料:

浏览器同源政策及其规避方法 - 阮一峰
前端必备HTTP技能之同源策略详解
同源政策 - 维基百科

在互联网中根本不存在真正的资源共享(也不能存在),所有正当的资源共享都是建立在其所有者的许可之下的。网站资源也是如此,一个网站通常包含了用户的各种信息,”同源策略”就是为了保证用户信息的安全,防止恶意的其他网站窃取数据。

试想:

A网站是一家银行,用户登录以后,其个人信息或者状态被保存下来,又去浏览其他网站。如果这个网站可以读取A网站的资源(包括 Cookie),那么会发生什么?

很显然,他被保存的个人隐私已经被泄漏了,更致命的是登录状态被泄漏,这意味着这个帐户已经被其他人掌握了,甚至可以以此为所欲为。这便是”同源策略“的意义目的所在,保护用户的个人隐私安全,不被其他网站窃取泄漏信息。

然而,事实上在很多时候开发一个网站会依赖其他域网站的资源(例如大型网站依赖多个子域),此时同源策略便成阻碍,而绕过或者规避同源策略便称为跨域。

对于跨域,简单的说就是向另一个的域(或者说不同的”源”)发出请求,访问它的敏感数据资源。

以上便是对域与跨域的简述。

2. 常见需要跨域的情况

要了解在什么情况下需要跨域,那么就需要知道”同源策略”究竟限制了谁和什么功能。

同源策略是浏览器最核心也最基本的安全功能,如果缺少了它,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

同源策略主要是浏览器的安全机制,在不同的域的情况下,为了保证安全,它通常限制了三种行为。

(1) Cookie、LocalStorage 和 IndexDB 无法读取。
(2) DOM 无法获得。
(3) AJAX 请求不能发送。

即一个域的资源不能读取另一个域的Cookie、LocalStorage 和 IndexDB,也不能获得它的 DOM 文档,更不能对其发送 AJAX 请求。

针对具体的情况有不同的规避方法:

  1. 浏览器端不同窗口之间跨域通信,这种情况主要包括子域,iframe,windows 通信
  2. 浏览器向不同域的服务器发起跨域请求,通常是发起 [XMLHttpRequest][] 请求

3. 浏览器端窗口之间跨域通信

同源策略对于父子域也是起效的,即便两个网页一级域名相同,只是二级域名不同

A网页是http://w1.example.com/a.html
B网页是http://w2.example.com/b.html

一个大型网站通常包括了数个子域,它们整体来看可能是一体的,而在做开发时理论上只需某个域保存信息就应该被同一网站的其他子域共享,实际上这也被同源策略限制了。其中主要就是 Cookie 的共享。

在窗口之间有以下需求时,我们需要绕过同源策略,即窗口跨域:

  1. 子域之间共享隐私数据,例如 Cookie、LocalStorage 和 IndexDB 无法读取。
  2. 获取一个网页的不同源 iframe 数据
  3. 通过 windows.open 打开窗口通信

窗口跨域的主要解决方法有:

  1. document.domain
  2. 片段识别符
  3. window.name
  4. 跨文档通信API

详细请参考浏览器同源政策及其规避方法 - 阮一峰,这里简单介绍:

3.1 document.domain

适用:父子域的 Cookie 和 iframe 窗口,一级域名相同,二三级不同的情况。
原理:Cookies 的作用域。
使用:两个不同域设置相同的 document.domain,主要共享 Cookies。

A域和B域同时设置

document.domain="example.com";

A域或B域设置 Cookie,都可以访问到该值

document.cookie = "test1=hello";

A域和B域都可以访问到作用域在 example.com 的 Cookies,以此实现共享。

服务器设置Cookie时,可以直接指定Cookie的所属域名为一级域名,比如 .example.com

Set-Cookie: key=value; domain=.example.com; path=/

这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。

3.2 片段识别符

适用:一个网页嵌入完全不同源的 iframe
原理:URL位置#(hash) 和 hashchange 事件
使用:

父或子(iframe)设置 hashchange 事件

window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  // ...
}

父向子:

var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;

子向父:

parent.location.href= target + "#" + hash;
3.3 window.name

适用:始终在同一窗口下,iframe 或者网页跳转
原理:浏览器窗口的 window.name 属性是持久、共享的,只要子同一窗口下,无论跳转到哪,嵌套多少 iframe ,始终共享一个 window.name 属性
使用:

B 网页设置 window.name

window.name = data;

跳转到或者嵌入的 A 网页:

//对于跳转
let data = window.name;
//对于 iframe (A 包含 B)
let data = iframe.contentWindow.name;
3.4 跨文档通信 API

以上方法是建立在既有机制的特性上的取巧行为,它们不是最初的目的并不是跨域通信,只是在某种情况下能够更简单的做到。

HTML5 为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

详细请参考:

window.postMessage - MDN

适用:本窗口有通信目标的窗口的引用,比如 iframe 的contentWindow属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames 等。能够访问网页的所有,包括 web DB.
使用:

  1. 获取目标窗口引用

    iframe 的 contentWindow

    var target = iframe.contentWindow;

    window.open

    var target = window.open(target_url, title);
  2. 设置监听事件

    window.addEventListener('message', function);
    
    // OR
    
    window.onmessage = function;
  3. 发送信息

    父向子

    target.postMessage(msg,target_origin);

    子向父

    window.opener.postMessage(msg, parent_origin);

4. 跨域请求

服务器与客户端跨源通信的常用方法包括:

  • JSONP
  • WebSocket
  • 跨域资源共享 CORS
  • 代理
4.1 JSONP

JSONP 基本原理 script 标签的 src 没有被同源策略限制,那么可以动态创建 script 标签,然后利用 src 属性进行跨域,最后返回的数据在该标签中完成处理,但是 src 发出的请求实质上为 get 方式,这也是 JSONP 无法用 post 请求的原因,当然在同域可以使用 post 是因为没有创建 script 标签。

jsonp并不是ajax请求,本质上 jsonp 欺骗了浏览器
代码:我要载一个js文件
浏览器:好的
代码悄悄的拿到数据,然后说了一句:这个SB…….

—— jsonp为什么不支持post请求? - 知乎 (小羊装狼)

简单实现:

function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};

通过 ajax

$.ajax({
    url:'http://example.com/ip',
    type:'get',  //post等依旧会被转换为 get
    dataType:'jsonp',  
    jsonpCallback:'foo'  //获取数据的函数
});
4.2 WebSocket

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议 不实行同源政策只要服务器支持 ,就可以通过它进行跨源通信。

WebSocket请求的头信息中,有一个字段是 Origin ,表示该请求的请求源(origin),即发自哪个域名。

正是因为有了Origin这个字段,所以WebSocket才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。

详细可以参考:
浏览器同源政策及其规避方法 - 阮一峰
浅谈WEB跨域的实现(前端向)

PS:因为暂未接触配置服务器块的WebSocket相关,所以暂时不能进行测试,该问题留待二次学习时解决。

4.3 服务器代理

浏览器有跨域限制,但是服务器不存在跨域问题,所以可以由服务器请求所要域的资源再返回给客户端。

服务器代理是万能的。

简单的代理实现
java:java使用代理解决跨域问题
nodejs:跨域之Server Proxy

4.4 跨域资源共享 CORS

CORS是一个W3C标准,目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

CORS 的原理或者说核心思想:

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

这组 HTTP 首部字段:

  • 请求头
    • Origin
      : 必须,表明预检请求或实际请求的源站
    • Access-Control-Request-Method
      : 用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器
    • Access-Control-Request-Headers
      : 用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器
  • 响应头
    • Access-Control-Allow-Origin
      : 必须,允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求
    • Access-Control-Allow-Methods
      : 用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法
    • Access-Control-Allow-Headers
      : 用于预检请求的响应。其指明了实际请求中允许携带的首部字段。
    • Access-Control-Allow-Credentials
      : 用于告知浏览器是否需要保存或者发送凭证信息
    • Access-Control-Expose-Headers
      : 让服务器把允许浏览器访问的头放入白名单
    • Access-Control-Max-Age
      : 指定了preflight请求(预检请求)的结果能够被缓存多久

说的简单点就是:

网页在浏览器向服务器发送跨域请求时,浏览器直接在该请求中标明网页和请求的信息,让服务器自己判断需不需要处理;又或者在复杂情况下,浏览器先询问服务器可不可处理某些请求,如果可以再发送具体的请求。

浏览器将CORS请求分成两类:

  • 简单请求(simple request)
  • 非简单请求(not-so-simple request)

只要同时满足以下两大条件,就属于简单请求。

(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的头信息不超出以下几种字段:

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

凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。

对于简单请求,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段,如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

请求报文和响应报文示例:

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[XML Data]

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

与前述简单请求不同,“需预检的请求”(非简单请求)要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。”预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

非简单请求的请求报文和响应报文:

GET /resources/access-control-with-credentials/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/credential.html
Origin: http://foo.example
Cookie: pageAccess=2


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2.0.61 (Unix) PHP/4.4.7 mod_ssl/2.0.61 OpenSSL/0.9.7e mod_fastcgi/2.4.2 DAV/2 SVN/1.4.2
X-Powered-By: PHP/5.2.6
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain


[text/plain payload]

关于跨域资源共享,详细请参考:
跨域资源共享 CORS 详解
CORS - 思维导图
HTTP访问控制(CORS)

CORS 内容比较多,此次只是对其进行总结,会另开一贴对其内部机制进行详细介绍,完成将链接到此处

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值