网络基础18 跨域知识总结
跨域知识总结。
跨域及产生原因
什么是跨域
跨域是指从一个域名的网页去请求另一个域名的资源。比如从www.baidu.com
页面去请求www.google.com
的资源。
跨域的严格一点的定义是:协议(http
&https
)、端口(80
&81
)、域名(baidu
&google
)、二级域名(news
&sports
)不相同,都为跨域。
受到跨域的限制,浏览器不能用AJAX获取不同源的数据,也不能在同一个页面但是处于不同域的框架之间的进行数据传递。
1 | URL 说明 是否允许通信 |
为什么浏览器要限制跨域访问
==首先要注意,跨域是浏览器的限制,服务端不存在跨域的问题~==
原因就是安全问题:如果一个网页可以随意地访问另外一个网站的资源,那么就有可能在客户完全不知情的情况下出现安全问题。比如下面的操作就有安全问题:
用户访问
www.mybank.com
,登陆并进行网银操作,这时浏览器会生成对应的cookie。用户突然想起件事,并迷迷糊糊地访问了一个邪恶的网站www.xiee.com
,这时该网站就可以在它的页面中,拿到银行的cookie,cookie中可能存放着很多敏感信息比如用户名,登陆token等,然后发起对www.mybank.com
的操作。如果这时浏览器不予限制,并且银行也没有做响应的安全处理的话,那么用户的信息有可能就这么泄露了。
为什么要跨域
有时公司内部有多个不同的子域,比如一个是location.company.com
, 而应用是放在app.company.com
, 这时想从app.company.com
去访问location.company.com
的资源就属于跨域。
不受跨域限制的情况
有两种情况是不受浏览器同源策略的限制的,第一种是HTML标签中像<srcipt>
、<img>
等获取资源的标签是没有跨域限制的,第二种是提交表单是不受跨域的限制的。
要注意的是,表单提交只有通过指定表单的action
这种形式才是不受跨域限制的,而通过Ajax手动控制表单提交的情况还是会收到跨域的限制的。
这是因为通过action
提交表单,剩余的操作就交给了action
里面的域,本页面的逻辑和这个表单没关系,不需要表单里的域的吓响应,所以浏览器认为是安全的。
而使用Ajax来发送表单的请求的时候,页面JS会需要知道请求的返回值,这个时候表单的情况会收到跨域的限制。
跨域限制的是哪个步骤
当我们尝试发送一个跨域请求的时候,会被浏览器阻止,在控制台收到错误信息:
我们是收不到对方服务器的返回结果的,但是浏览器阻止的究竟是请求的发出还是响应的读取呢?
首先使用Koa2搭建一个HTTP服务,运行在8080
端口:
1 | const Koa = require('koa'); |
前端页面运行在http://localhost:63342
,当页面访问http://localhost:8080
进行跨域请求接口数据的时候,浏览器由于跨域的原因报错,没有收到服务器响应的数据,并且cookie也没有写入成功。
但是服务端的控制台会显示这次请求的信息:
证明,浏览器的同源策略,并没有限制请求的发送,而是在浏览器接受响应数据前进行了检查和限制。
为什么这样限制呢?因为有可能服务端进行了允许特定网站的跨域请求,浏览器如果从发送请求就限制住,那么就不存在任何跨域的可能性了,所以需要根据服务端返回的结果在进行检查,是要限制还是将数据解析展示。
跨域的方法
跨域类型我个人觉得可以分为两种,一种是接口跨域,也就是需要跨域去请求接口,返回服务端的数据,是页面与服务端的通信;另外一种是页面跨域,是两个页面间的通信,可能是两个Tab间的通信,也可能是主页面与iframe之间的通信。
接口跨域的方法:
- 动态创建
script
- JSONP
- CORS
- Nginx反向代理跨域
- 利用后端转发请求跨域
<img>
标签- Websocket
- SSE + EventSource
页面跨域的方法:
document.domain
window.name
postMessage
接口跨域
(1)动态创建script
由于HTML标签中像<srcipt>
、<img>
等获取资源的标签是没有跨域限制的,利用这一点就可以实现跨域。
在本地定义一个方法cb
,在一个<srcipt>
标签中的src
访问跨域的目标地址,并通过查询参数将函数名传递给服务端。服务端在接受到请求后将数据作为cb
的参数,返回序列化的cb(params)
。
由于请求是由<script>
标签发出的,所以接受到服务端的结果后就会立即调用cb
,并且参数就会返回的数据。
前端代码:
1 | <script type="text/javascript"> |
服务端代码(使用Node):
1 | var http = require('http'); |
这种方式比较常用,只要服务端部署了响应的代码,前端按照约定传递一个callback
的查询参数即可,但是要注意的就是对XSS的防范,防止攻击者构造特殊的callback
代码,实现注入攻击。
可以将创建<script>
标签的过程封装起来
1 | function getJSON (url, callBack) { |
(2)JSONP
JSONP就是对上面的方法的一个封装,一种是get
/getJSON
的方式,另一种是$.ajax()
的方式,都支持GET请求,不支持POST请求。
前端代码:
1 | //getJOSN方法 |
第一种方式的callback=?
回调函数标志着Ajax请求是以JSONP的方式发送请求,客户端请求会自动在问号处增加一个方法名,方法名是以jquery开头的一串数字
而第二种方式的是参数dataType="jsonp"
来说明是以JSONP的方式发送请求,所以可以直接在callback
后面直接指定方法名callback=m1
,而不需要jsonpCallback: "m1"
这一行代码。
(3)CORS(跨域资源共享)
CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing),这种跨域方式如果不携带cookie跨域,那么前端发送请求不需要额外处理,如果需要携带cookie,那么需要添加withCredential: true
这个字段
1 | var xhr = new XMLHttpRequest(); |
主要的工作量都在后端代码,来设置响应头,如果是简单请求,那么只需要设置Access-Control-Allow-Origin
即可,如果是非简单请求那么需要同时设置Access-Control-Allow-Origin
、Access-Control-Request-Method
和Access-Control-Allow-Headers
,如果需要跨域那么还需要额外设置Access-Control-Allow-Credentials
一般都会使用专门的中间件来帮助我们完成,比如koa-cors。
(4)Nginx反向代理跨域
以前其实并不了解Nginx跨域的原理,其实就是利用了服务度不存在跨域的原理,前端JS访问自己本地的资源,而Nginx将访问跨域目标的请求进行了拦截,将请求转发到真实的地址,获取到数据。
上面的的方法都有一个共同的限制,那就是必须在要跨域的目标服务器上进行相应的部署,但是有一种最常遇到的情形,就是想要获取的数据的网站是不受自己控制的,开发者只能控制一个域。
这时候可以通过Nginx进行反向代理跨域,具体的配置可以参考这篇笔记《零散专题33 Nginx
(5)利用后端转发请求跨域
除了使用Nginx,也可以使用其他的框架来进行本地请求的转发,比如如果使用Node的Express框架编写的服务端程序,那可以使用request
模块进行请求转发,实现跨域请求。
1 | var request = require('request'); |
上面完整的请求传递给API,并且将相应传递给请求的发起者,支持GET/PUT/DELETE的其他请求方式
使用的时候,前端只需将需要跨域的请求请求到/api
地址,将真实地址放到查询参数中:
1 | export const QUERY_URL = 'https://wallstreetcn.com/live/global'; |
(6)<img>
标签
使用<img>
标签原理与使用<srcipt>
的原理是相同的,<img>
标签不存在跨域的问题,可以访问任意其他网页的图片,并且可以通过onload
和onerror
事件了解到响应是何时完成的
1 | let img = new Image(); |
当img
的src
被设置那一刻,请求就发出了,服务器的响应一般是像素图或204 No-Content
一般用来跟踪用户点击页面或动态广告曝光字数,是一种==单向的跨域方式==,==只能是GET请求==,并且无法访问服务器的相应文本。
(7)Websocket
Websocket建立的连接不存在跨域问题,因此可以通过建立Websocket打开到任何站点的俩进阶,至于是否可以与页面通信,完全取决于服务器(通过握手信息就可以知道请求来自何方)
前端代码:
1 | <div>user input: |
服务端Node代码:
1 | var http = require('http'); |
(8)SSE + EventSource
SSE(server-sent events)是服务器发送事件,是一种服务器向客户端推送的单向通信技术。它的API就是EventSource接口
URL可以与当前网址同域,也可以跨域。跨域时,可以指定第二个参数,打开withCredentials
属性,表示是否一起发送Cookie。
1 | if (window.EventSource) { |
页面跨域
(1)document.domain
+ iframe
适用于主域相同,子域不同的情况,并且需要对跨域的两个网页的JS脚本都进行修改。例如在http://www.a.com/a.html
和http://script.a.com/b.html
之间的跨域就可以使用这种方法。
主域名是不带www的域名,比如
a.com
, 主域名带前缀的通常是二级域名或者多级域名,比如www.a.com
。
首先在a.html
中添加下面的
1 | // a.html |
将b.html
通过iframe添加到a.html
页面下,也需要在b.html
中设置相同的domain
1 | // b.html |
这样就将两个原本跨域的网页的域统一了,此时就和平时同一个域镶嵌iframe一样,通过iframe的contentDocument
中就可以实现数据交互。
页面默认的
domain
是window.loaction.hostname
,document.domain
只能设置成自身或更高一级的父域,比如a.b.example.com
中的某个页面的document.domain
可以设置为a.b.example.com
、b.example.com
、example.com
,但是不能是设置为c.a.b.example.com
或baidu.com
。
因此为保证两个主域相同,子域不同的页面跨域,二者的document.domain
只能设置为二者的共同的主域,即a.com
这种方法依赖于iframe,iframe的缺点也是一堆,现在的大部分网站避免使用iframe。
历史上,iframe 常被用于复用部分界面,但是多数情况下并不合适。现在,应该使用iframe的例子如:
- 沙箱隔离。
- 引用第三方内容。
- 独立的带有交互的内容,比如幻灯片。
- 需要保持独立焦点和历史管理的子窗口,如复杂的Web应用。
缺点也很明显:大量使用,打开一个网页加载过多iframe体验很不友好而且影响网页加载速度,对爬虫不够友好。
(2) window.name
将JSON格式的数据写入到window.name
中,通过共享一个Tab窗口的两个页面的共同的window.name
实现跨域的数据传递
window.name
属性在一个窗口(window
)生命周期内,窗口载入的所有的页面都是共享一个window.name
的,每个页面对window.name
都有读写权限。
window.name
是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而重置。window.name
只能是字符串,最大允许2M左右数据。
这种方法的限制太多,不展开了。
(3)postMessage
页面和其打开的新窗口的数据传递、页面与嵌套的iframe消息传递都可以使用postMessage
方法跨域或同源传递数据。
postMessage()
方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
1 | window.postMessage(message, targetOrigin); |
message
是要发送的数据,可以是JavaScript的任意基本类型或可复制的对象,但是出于兼容性考虑,最好使用JSON.stringify()
方法对对象参数序列化。targetOrigin
用来限制otherWindow
对象所在的域,指明目标窗口的URL,可以将参数设置为”*
“传递给任意窗口,如果要指定和当前窗口同源的话设置为”/
“。
接受消息的页面通过通过监听window
的message
事件获取传过来的消息,消息内容存储在message
事件对象的data
属性中。
1 | window.addEventListener("message", function(e){ |
(4)代码示例
a.com
上的a.html
的代码:
1 | <iframe id="ifr" src="b.com/b.html"></iframe> |
b.com
上的b.html
的代码:
1 | <script type="text/javascript"> |