什么是跨域?
跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。
广义的跨域:
1.) 资源跳转: A链接、重定向、表单提交
2.) 资源嵌入: <link>、<script>、<img>、<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链
3.) 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等
其实我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。
什么是同源策略?
同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制以下几种行为:
1.) Cookie、LocalStorage 和 IndexDB 无法读取2.) DOM 和 Js对象无法获得3.) AJAX 请求不能发送
1、 通过jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域
1.jsonp
前端代码: 工作在8080端口
<script>
function jsonp({url,params,callback}){
return new Promise((resolve,reject)=>{
let script=document.createElement('script');
params={...params,callback};
var arr=[];
for(var [k,v] of Object.entries(params)){
arr.push(`${k}=${v}`)
}
console.log(arr);
script.src=`${url}?${arr.join('&')}`
document.body.appendChild(script)
window[callback]=function(data){
resolve(data);
document.body.removeChild(script)
}
})
}
jsonp({
url: 'http://localhost:5000/say',
params: { wd: 'Iloveyou' },
callback: 'show'
}).then(data => {
console.log(data)
})
</script>
后端:
// jsonp 跨域
router.get('/say', function (req, res) {
let {
wd,
callback
} = req.query
console.log(wd)
console.log(callback)
res.end(`${callback}('我不爱你')`)
})
2.cors
假如站点 foo.example 的网页应用想要访问 bar.other 的资源。foo.example 的网页中可能包含类似于下面的 JavaScript 代码:
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/public-data/';
function callOtherDomain() {
if(invocation) {
invocation.open('GET', url, true);
invocation.onreadystatechange = handler;
invocation.send();
}
}
客户端和服务器之间使用 CORS 首部字段来处理跨域权限:
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 //请求的原始资源URI
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]
第 1~10 行是请求首部。第10行 的请求首部字段 Origin 表明该请求来源于 http://foo.exmaple。
第 13~22 行是来自于 http://bar.other 的服务端响应。响应中携带了响应首部字段 Access-Control-Allow-Origin(第 16 行)。使用 Origin 和 Access-Control-Allow-Origin 就能完成最简单的访问控制。本例中,服务端返回的 Access-Control-Allow-Origin: * 表明,该资源可以被任意外域访问。如果服务端仅允许来自 http://foo.example 的访问,该首部字段的内容如下:
Access-Control-Allow-Origin: http://foo.example
现在,除了 http://foo.example,其它外域均不能访问该资源(该策略由请求首部中的 ORIGIN 字段定义,见第10行)。Access-Control-Allow-Origin 应当为 * 或者包含由 Origin 首部字段所指明的域名。
3.postMessage + iframe
1.页面和其打开的新窗口的数据传递
2.多窗口之间消息传递
3.页面与嵌套的 iframe 消息传递
上面三个场景的跨域数据传递
otherWindow.postMessage(message, targetOrigin, [transfer]);
1.message: 将要发送到其他 window 的数据。
2.targetOrigin:通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个 URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
3.transfer(可选):是一串和 message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
接下来我们看个例子: http://localhost:3000/a.html页面向http://localhost:4000/b.html传递“我爱你”,然后后者传回"我不爱你"。
// a.html
< iframe src = "http://localhost:4000/b.html"frameborder = "0"id = "frame"onload = "load()" > </iframe/ / 等它加载完触发一个事件
//内嵌在http://localhost:3000/a.html
< script >
function load() {
let frame = document.getElementById('frame') frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
window.onmessage = function(e) { //接受返回数据
console.log(e.data) //我不爱你
}
} < /script>
/ / b.html window.onmessage = function(e) {
console.log(e.data) //我爱你
e.source.postMessage('我不爱你', e.origin)
}
4. websocket
Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket 和 HTTP 都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。 原生 WebSocket API 使用起来不太方便,我们使用Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口,也对不支持 webSocket 的浏览器提供了向下兼容。 我们先来看个例子:本地文件 socket.html 向localhost:3000发生数据和接受数据.
// socket.html
< script >
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function() {
socket.send('我爱你'); //向服务器发送数据
}
socket.onmessage = function(e) {
console.log(e.data); //接收服务器返回的数据
}
< /script>
/ / server.js
let express = require('express');
let app = express();
let WebSocket = require('ws'); //记得安装ws
let wss = new WebSocket.Server({
port: 3000
});
wss.on('connection',
function(ws) {
ws.on('message',
function(data) {
console.log(data);
ws.send('我不爱你')
});
})
5. Node 中间件代理(两次跨域)
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。
2、 vue框架的跨域(1次跨域)
利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。
webpack.config.js部分配置:
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // 代理跨域目标接口
changeOrigin: true,
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}],
noInfo: true
}
}
6.document.domain + iframe
该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。 只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。 实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。 我们看个例子:页面a.zf1.cn:3000/a.html获取页面b.zf1.cn:3000/b.html中 a 的值
// a.html
< body >
helloa
< iframe src = "http://b.zf1.cn:3000/b.html"frameborder = "0"onload = "load()"id = "frame" > </iframe>
<script>
document.domain = 'zf1.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script >
</body>
/ / b.html < body > hellob < script > document.domain = 'zf1.cn'
var a = 100; < /script>
</body >
7.nginx 反向代理
实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。 使用 nginx 反向代理实现跨域,是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。 实现思路:通过 nginx 配置一个代理服务器(域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域登录。 先下载nginx,然后将 nginx 目录下的 nginx.conf 修改如下:
// proxy服务器
server {
listen 80;
server_name www.domain1.com;
location / {
proxy_pass http: //www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com;#修改cookie里域名index index.html index.htm;
#当用webpack - dev - server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用add_header Access - Control - Allow - Origin http: //www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access - Control - Allow - Credentials true;
}
}
最后通过命令行nginx - s reload启动nginx
// index.html
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
// server.js
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request',
function(req, res) {
var params = qs.parse(req.url.substring(2));
// 向前台写cookie
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
多标签页通信
- localstorage
localstorage是浏览器多个标签共用的存储空间,所以可以用来实现多标签之间的通信(ps:session是会话级的存储空间,每个标签页都是单独的)
直接在window对象上添加监听即可:
window.onstorage = (e) => {console.log(e)}// 或者这样window.addEventListener('storage', (e) => console.log(e))
直接在window对象上添加监听即可:window.onstorage = (e) => {console.log(e)}// 或者这样window.addEventListener('storage', (e) => console.log(e))onstorage以及storage事件,针对都是非当前页面对localStorage进行修改时才会触发,当前页面修改localStorage不会触发监听函数。然后就是在对原有的数据的值进行修改时才会触发,比如原本已经有一个key会a值为b的localStorage,你再执行:localStorage.setItem('a', 'b')代码,同样是不会触发监听函数的。
- websocket
- cookie和localstorage。前者用setInterval进行订阅,后者可以监听storage事件。
- 定时器setInterval+cookie在页面A设置一个使用 setInterval 定时器不断刷新,检查 Cookies 的值是否发生变化,如果变化就进行刷新的操作。由于 Cookies 是在同域可读的,所以在页面 B 审核的时候改变 Cookies 的值,页面 A 自然是可以拿到的。这样做确实可以实现我想要的功能,但是这样的方法相当浪费资源。虽然在这个性能过盛的时代,浪费不浪费也感觉不出来,但是这种实现方案,确实不够优雅.