跨域通信
跨域通信
同源策略和跨域
同源策略:限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。
同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS
、CSRF
等攻击。所谓同源是指”协议+域名+端口“三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制内容有:
Cookie
、LocalStorage
、IndexedDB
等存储性内容DOM
节点AJAX
请求
但是有的标签是允许跨域加载资源:
<img src=XXX>
<link href=XXX>
<script src=XXX>
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。
特别说明两点:
第一:如果是协议和端口造成的跨域问题“前台”是无能为力的。
第二:在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”可以理解为“协议, 域名和端口必须匹配”。
这里你或许有个疑问:请求跨域了,那么请求到底发出去没有?
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。
Ajax
Asynchronous Javascript And XML是一种异步请求数据的web开发技术,对于改善用户的体验和页面性能很有帮助。简单地说,在不需要重新刷新页面的情况下,Ajax 通过异步请求加载后台数据,并在网页上呈现出来。
原生Ajax
发送 Ajax 请求的五个步骤
XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。
创建
XMLHttpRequest
对象。const xhr = new XMLHttpRequest()
使用
open
方法设置请求的参数。xhr.open(method, url, 是否异步)
。get
可以把请求参数拼接在url
如果想要使用
post
提交数据,必须添加此行。xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
发送请求。
get
请求:xhr.send()
post
请求:将数据通过send
方法传递xhr.send('name=fox&age=18');
注册事件。 注册
onreadystatechange
事件,状态改变时就会调用。xhr.readyState===4
(xhr.status>=200 && xhr.status<300) || xhr.status === 304
304:服务器端资源未改变,可直接使用客户端未过期的缓存。
获取返回的数据,更新UI。
onreadystatechange 事件
注册 onreadystatechange
事件后,每当 readyState
属性改变时,就会调用 onreadystatechange
函数。
readyState
:(存有 XMLHttpRequest
的状态。从 0 到 4 发生变化)
0: 请求未初始化
1: 服务器连接已建立
2: 请求已接收
3: 请求处理中
4: 请求已完成,且响应已就绪
兼容性
IE浏览器通过 XMLHttpRequest
或者 ActiveXObject
(IE6及更低版本浏览器)对象同样实现了ajax的功能。
Promise封装axios
axios是一个基于promise
的HTTP
库,可以用在浏览器
或者node.js
中。
axios是通过promise实现对ajax技术的一种封装。
axios提供两个http请求适配器,XHR和HTTP。XHR的核心是浏览器端的XMLHttpRequest对象;HTTP的核心是node的http.request方法。
特性:
- 从浏览器中创建XMLHttpRequests
- 从node.js创建http请求
- 支持promise API
- 拦截请求与响应
- 转换请求数据与响应数据
- 取消请求
- 自动转换JSON数据
- 客户端支持防御XSRF
1 | /** |
跨域解决方案
JSONP
JSONP原理
JSON with Padding
JS函数包裹JSON数据。
利用 <script>
标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。
<script>
标签中type的默认属性是text/javascript
,请求内容就会被浏览器以JS代码进行执行。
JSONP
和AJAX
相同,都可以从客户端向服务器端发送请求获取数据。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)。
JSONP优缺点
JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。
缺点是仅支持get方法,具有局限性;不安全,可能会遭受XSS攻击。
JSONP的实现流程
- 声明一个回调函数,其函数名(如
jsonpCallback
)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。 - 创建一个
<script>
标签,把那个跨域的API数据接口地址,赋值给<script>
的src
,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=jsonpCallback
)。 - 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串。
- 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(
jsonpCallback
),对返回的数据进行操作。
1 | //去创建一个script标签 |
在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要一个封装好的JSONP函数。
React调用jsonp的库
yarn add jsonp
1 | import jsonp from 'jsonp' |
JSONP请求的简单例子
客户端 http://127.0.0.1:5500
1 | <body> |
服务器 http://127.0.0.1:8000
1 | //1. 引入express |
CORS
CORS(Cross-Origin Resource Sharing),跨域资源共享。
CORS是官方的跨域解决方案,需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
通过设置一个响应头来告诉浏览器,该请求允许跨域,浏览器收到该响应以后就会对响应放行。
浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
服务端设置 Access-Control-Allow-Origin
就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符*
则表示所有网站都可以访问资源。
虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。
简单请求
只要同时满足以下两大条件,就属于简单请求
请求方法是 HEAD
、GET
、POST
三种之一;
HTTP 头信息不超过右边着几个字段:Accept
、Accept-Language
、Content-Language
、Last-Event-ID
Content-Type
只限于三个值 application/x-www-form-urlencoded
、multipart/form-data
、text/plain
;
复杂请求
不符合以上条件的请求,比如请求方法是 PUT
或 DELETE
,或 Content-Type
值为 application/json
。浏览器会在正式通信之前,发送一次 HTTP 预检 OPTIONS
请求,先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 请求方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XHR
请求,否则报错。
CORS完整复杂请求的例子
客户端 http://127.0.0.1:5500
1 | <body> |
服务器 http://127.0.0.1:8000
1 | //1. 引入express |
第三方中间件
1 | const express = require('express'); |
关于CORS的 cookie 问题
想要传递 cookie
需要满足 3 个条件
- web 请求设置
withCredentials
这里默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials
来进行传递 cookie
.
1 | // 原生 xml 的设置方式 |
2.Access-Control-Allow-Credentials
为 true
3.Access-Control-Allow-Origin
为非 *
postMessage
postMessage
是HTML5 XMLHttpRequest Level 2
中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
上面三个场景的跨域数据传递。
postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
1 | otherWindow.postMessage(message, targetOrigin, [transfer]); |
message
: 将要发送到其他window的数据。targetOrigin
:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串”*
“(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin
提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。transfer
(可选):是一串和message
同时传递的Transferable
对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
简单例子
客户端 http://127.0.0.1:5500
1 | <body> |
服务器 http://127.0.0.1:8000
1 | <body> |
WebSocket
WebSocket
是HTML5
的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。
WebSocket
和HTTP
都是应用层协议,都基于 TCP
协议。
但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。
同时,WebSocket
在建立连接时需要借助 HTTP
协议,连接建立好了之后 client
与 server
之间的双向通信就与 HTTP
无关了。
简单例子
客户端 http://127.0.0.1:5500
1 | <body> |
服务器 http://localhost:4000
1 | const WebSocket = require('ws');//记得安装ws |
代理
简单的说,一般给客户端做代理的都是正向代理,给服务器做代理的就是反向代理。
Node中间件代理
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。
代理的思路为,利用服务端请求不会跨域的特性,让接口和当前站点同域。
代理服务器,需要做以下几个步骤:
- 接收客户端请求 。
- 将请求转发给服务器。
- 拿到服务器响应数据。
- 将响应转发给客户端。
React中配置代理
在
package.json
中追加如下配置 :"proxy":http://localhost:5000
当你请求
http://localhost:5000
产生跨域(本身在3000
端口)时,添加此代码, 之后你请求时用http://localhost:3000
进行请求,当其在3000
端口中找不到资源时将会自动转发至5000
端口进行请求,不产生跨域问题。优点:配置简单,前端请求资源时可以不加任何前缀。
缺点:不能配置多个代理
工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)
在
src
下创建配置文件:src/setupProxy.js
ps:必须是这个文件名,react项目运行的时候会自动查找这个文件,并将其加入webpack的配置中,所以当你修改此文件后,你需要重新启动项目
优点:可以配置多个代理,可以灵活的控制请求是否走代理。
缺点:配置繁琐,前端请求资源时必须加前缀。
1 | //代码示例 |
Nginx反向代理
测试时注意,Nginx设置了跨域后,服务器不要再设置CORS。
实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。
使用Nginx反向代理实现跨域,是最简单的跨域方式。只需要修改Nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
简单例子
先下载nginx,然后将nginx目录下的nginx.conf
修改如下:
1 | // proxy服务器 |
Nginx常用命令(win)
cmd 进入Nginx解压目录 执行以下命令
start nginx
: 启动nginx服务关闭cmd窗口是不能结束nginx进程的
- 快速停止或关闭Nginx:
nginx -s stop
- 正常停止或关闭Nginx:
nginx -s quit
- 杀死所有Nginx进程:
killall nginx
- 快速停止或关闭Nginx:
配置文件修改重装载命令:
nginx -s reload
常见问题:端口是否占用、Nginx是否结束
客户端 http://127.0.0.1:5500
1 | <body> |
服务器 http://127.0.0.1:8000
1 | //1. 引入express |
window.name + iframe
window.name
属性的独特之处:name
值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的name
值(2MB
)。
其中a.html
和b.html
是同域的,都是http://127.0.0.1:5500
;而c.html
是http://127.0.0.1:8000
a.html
1 | <body> |
b.html
为中间代理页,与a.html
同域,内容为空。
c.html
1 | <script> |
总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name
从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
location.hash + iframe
实现原理: a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash
传值,相同域之间直接js访问来通信。
document.domain + iframe
该方式只能用于二级域名相同的情况下,比如 a.test.com
和 b.test.com
适用于该方式。 只需要给页面添加 document.domain ='test.com'
表示二级域名都相同就可以实现跨域。
实现原理:两个页面都通过js强制设置document.domain
为基础主域,就实现了同域。
总结
- CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案。
- JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
- 不管是Node中间件代理还是Nginx反向代理,主要是通过同源策略对服务器不加限制。
- 日常工作中,用得比较多的跨域方案是CORS和Nginx反向代理。