写在前面
跨域就是非同源请求:ruanyifeng.com/blog/2016/04/same-origin-policy.html
跨域概念:跨域是指一个域下的文档或脚本试图去请求另一个域下的资源(比如从a站点获取一个页面,然后在这个页面上去访问b站点的资源,就会发生跨域)
同源:相同协议,相同域名,相同端口三者相同被称为同源;反之不同源;同源情况下就不存在跨域了。即便两个不同的域名指向同一个ip地址,也非同源。
同源是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击
同源限制:
1.) Cookie、LocalStorage 和 IndexDB 无法读取
2.) DOM 和 Js对象无法获得
3.) AJAX 请求不能发送
备注:浏览器对通过script标签访问其他域名是没有限制的。(比如平时jquery库的引入,就是script标签跨域,但是浏览器没有限制)
// 跨域示例
客户端以XMLHttpRequest对象访问`http://www.tianmao.com`, 天猫返回给客户端,
在`http://www.tianmao.com`域名网页中,有一端js代码请求`http://www.jd.com`,
这时候请求京东网址,会发生跨域,因为浏览器有安全限制,请求发出去,但是数据回不来;
浏览器对XMLHttpRequest对象跨域请求有限制,但是对script标签没有限制
跨域9种解决方案:
1、 通过jsonp跨域(script标签跨域)
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域(中间server转发)
8、 nodejs中间件代理跨域(中间server转发)
9、 WebSocket协议跨域(新协议不受跨域限制)
跨域鼻祖
最开始针对跨域,需要人为手动修改host。比如当前端本地请求的后端的数据接口是http:api.xx.com/getData,但是本地浏览器直接访问会跨域;此时可以在本地起一个web服务地址http:127.0.0.1:1111/index.html,然后修改host文件,使得前端代码中请求api.xx.com接口时,让请求打到127.0.0.1:1111服务地址上。这样让本地服务器去访问远程服务器就不会发生跨域,因为跨域不存在后端服务器之间
jsonp
script、img、link、iframe等不存在跨域请求的限制;而且这里所有的资源文件请求都是get请求
jsonp的底层原理:通过script标签发送请求,利用 <script> 标签没有跨域限制的漏洞
jsonp跨域弊端:
- 只支持
get请求方式。因为它是使用script标签去发送请求的;而且服务器端需要做处理,客户端也需要做处理;如果跨域时传递的参数非常多,那么这种方式不可取。 - 不安全。server返回的是浏览器直接执行的
func("data"),不管返回什么,浏览器都会直接执行,如果返回的是木马程序,那。。。。;而且数据是通过url的问号传参的方式,很容易被url劫持。
jsonp优点: 兼容性好,可用于解决主流浏览器的跨域数据访问的问题
JSONP和AJAX比对:JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
<!-- 示例1:此时服务器是返回数据了,但是报错语法错误SyntaxError,因为我们是以script的方式请求的,所以服务器返回的数据,客户端就会以js的方式去解析 -->
<script>
document.querySelector('input').onclick = function(){
var script = document.creatElement("script");
script.src = "http://www.jd.com/index.php";
document.body.appendChild(script);
}
</script>
<!-- 示例2:可以将客户端的回调函数作为参数传递给服务器端,让服务器端返回函数调用的字符串形式 -->
<script>
function getInfo(data){
// 处理服务器返回数据
console.log(data);
}
document.querySelector('input').onclick = function(){
var script = document.creatElement("script");
script.src = "http://www.jd.com/index.php?callback=getInfo"; // 需要服务器返回 "getInfo(返回数据)" 这样客户端就可以以js代码解析成函数调用处理
document.body.appendChild(script);
}
</script>
CORS跨域资源共享
普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。
需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。
兼容性:目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。
CORS跨域原理:比如客户端在127.0.0.1:5000/index.html页面访问服务端127.0.0.1:3000地址,此时发生跨域,服务端可以在响应头中,设置Access-Control-Allow-Origin:127.0.0.1:5000,允许127.0.0.1:5000这个地址的客户端访问自己。
注意点:在cors跨域方式中,客户端会向服务端先进行一次options试探性请求,服务端看到这个请求,会返回表示同意接下来的跨域请求。
步骤要点:
1. 客户端(ajax/fetch发送请求)
2. 服务端设置相关头信息(比如允许跨域的域名,允许跨域的方法,允许跨域的cookie凭证)
3. 服务端还需要处理options试探性请求
缺点:
Access-Control-Allow-Origin只能设置成* / 一个具体的域名地址,;如果设置成*号就不能携带cookie。 因为所有的源都能请求,cookie被传来传去的,浏览器为了保证安全性,所以不允许携带cookie。
http-proxy
一般在webpack、webpack-dev-server中配置; 代理的思想基本都是起一个中间webserver,前端先把请求打到webserver上,然后webserver再请求后端;因为服务到服务之间不存在跨域。
// src/client.js
// 模拟测试client
// 注意这里默认请求的是 http://localhost:8080,但是server.js中启动的是http://localhost:3000,所以跨域了访问不到,
// 提供思路,可以先让client把请求发到webpack的devServe上,devServer是http://localhost:8080服务,然后再让devServer转发到server.js的http://localhost:3000服务上
let xhr = new XMLHttpRequest()
xhr.open('GET', '/api/user', true)
xhr.onload = () => {
console.log(xhr.response);
}
xhr.send()
// src/server.js
let express = require('express')
let app = express()
app.get('/api/user', (req, res) => {
res.send("我是服务端的返回信息hello")
})
app.listen(3000, '127.0.0.1') // 这里的ip可以省略,默认监听的localhost
// webpack.config.js
devServer: {
proxy: {
'/api': 'http://localhost:3000' // 给devServe配置代理,devServer不设置port默认在8080启动,这里表示对http://localhost:8080域名下以/api开头的,会被代理到http://localhost:3000 域名下
}
}
nginx反向代理
核心思想还是搞个服务器做一层中间代理 比如本地访问后端的地址是
www.xxx.com/api,但是可能会发生跨域,可以在本地搭个nginx,在nginx中搭个代理服务,然后让本地服务直接请求ng代理服务,然后ng代理服务再把请求转发给远程服务器。
// client.js
正常请求远程服务: www.xxx.com/api
将本地代码请求修改为ng监听地址: `127.0.0.1:80/api`,这样就访问到ng,然后打到 `locaiotn /api/`,ng代理的服务就会去请求`http:www.xxx.com/api`
// 在本地配置nginx.conf
server: {
listen:80
server_name: 127.0.0.1
locaiotn /api/ {
proxy_pass http:www.xxx.com/api
}
}
postMessage
引入了一个全新的API:跨文档通信 API(Cross-document messaging)。
因为两个窗口之间不是同源网页,那么没办法通信和共享cookie,storage等。window.postMessageAPI为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。
举例来说,父窗口 aaa.com 向子窗口 bbb.com 发消息,调用postMessage方法就可以了。
API简介:
window.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即协议 + 域名 + 端口。也可以设为*,表示不限制域名,向所有窗口发送。
- 父窗口和子窗口都可以通过
message事件,监听对方的消息。
window.addEventListener('message', function(e) {
console.log(e.data);
},false);
message事件的事件对象event,提供以下三个属性:
event.source:发送消息的窗口
event.origin: 消息发向的网址
event.data: 消息内容
我们这边假设我两个页面,A,B,目的是在B窗口中点击postMessage按钮,能够在A页面收到发来的消息
// A页面
<script>
function test() {
let op = window.open('b.html', '_blank');
function receiveMessage(event) {
console.log('event', event);
}
op.addEventListener("message", receiveMessage, false);
}
</script>
<body>
<div>
<button onClick="test()">open</button>
</div>
</body>
// B页面
<script>
function post() {
window.postMessage("hi there!", location.origin);
}
function receiveMessage(event) {
console.log('event', event)
}
window.addEventListener("message", receiveMessage, false);
</script>
<body>
<div>
<button onClick="post()">postMessage</button>
</div>
</body>
webSocket
www.ruanyifeng.com/blog/2017/0…
初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起,不能从服务端推送数据到客户端;websocket就是一种不仅能从客户端发送数据到服务端,也可以主动从服务的推送数据给客户端的一种全双工通信协议。
websocket是一种网络通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀,浏览器对这种通信协议没有跨域限制。
// websocket特点
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
ws://example.com:80/some/path
// client.js
// 执行new WebSocket("wss://echo.websocket.org")就会去连接wss://echo.websocket.org服务器。
var ws = new WebSocket("wss://echo.websocket.org");
// 成功的回调
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};