跨域
跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。
同源策略(Sameoriginpolicy) 是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。
所谓 同源 (即指在同一个域)就是两个页面具有相同的 协议
**(protocol),主机
**(host)和 **端口号
**(port)
跨域的解决方法
1. jsonp
我们在做项目的时候是不是经常会通过script
标签来引入资源,但是
1
| <script src="https://unpkg.com/vue/dist/vue.js"></script>
|
我们先引入一个vue文件,然后看看network

可以发现script标签的src属性可以跨域,其实不止script标签可以跨域,还有<img>
<link>
标签。
jsonp的原理其实就是通过script标签的src属性来实现的。
我们来模拟实现一下,后端服务器用 koa2 搭建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| router.get('/string', async (ctx, next) => { const {name,age} = ctx.request.query const data = `我叫${name},今年${age}岁!` ctx.body = `func(${JSON.stringify(data)})` })
var script = document.createElement('script'); script.src = 'http://localhost:3000/string?name=leo&age=30&callback=func'; document.body.appendChild(script) function func(res){ console.log(res) }
|
仔细看src其实跟普通的get请求差不多,唯一就是多了个callback,而callback才是重点,其作用是为了接收接口请求回来的数据。
当我们执行完代码,可以发现控制台输出了后端返回给我们的数据,我们再看看f12的source,发现执行了该回调函数并带上了后端返回的数据给我们。

这样就实现了跨域,整个过程其实就是前端定义回调函数,后端返回回调函数并带上数据。
缺点
需要前后端协商好回调函数命名,并且只支持get请求
2. CORS
跨域资源共享(Cross-Origin Resource Sharing)是一种机制,用来允许不同源服务器上的指定资源可以被特定的Web应用访问。
举个例子:如果A站的网页a.com
访问B站网页b.com
的API的时候,B站能够返回响应头Access-Control-Allow-Origin: http://a.com
,那么,浏览器就允许A站的JavaScript访问B站的API。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
const cors = require('koa-cors'); app.use(cors())
router.get('/string', async (ctx, next) => { ctx.body = `我叫笨鸟` })
var xmlhttp; if (window.XMLHttpRequest) { xmlhttp=new XMLHttpRequest(); } else { xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); } xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState==4 && xmlhttp.status==200) { console.log(xmlhttp.responseText) } } xmlhttp.open("GET","http://localhost:3000/string",true); xmlhttp.send();
|

浏览器发现这次跨域 AJAX 请求是简单请求,就自动在头信息之中,添加一个Origin
字段。服务器会在返回的头部信息中添加Access-Control-Allow-Origin
字段,浏览器会根据该字段与当前页面域名进行判断,如果一致就会放行。
优点
CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
3. websocket
WebSocket是一种在单个TCP连接上进行全双工通信的协议。
下面是一个websocket请求的的请求头

Connection: Upgrade
:表示要升级协议
Upgrade: websocket
:表示要升级到websocket协议
Sec-WebSocket-Key
:与服务端响应的头部的Sec-webSocket-Accept是配套的,提供基本的防护,比如恶意链接或者无意链接
Sec-WebSocket-Version
:websocket版本,如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
websocket为什么没有同源限制?
因为websocket使用类似ws://这样的方式进行连接,并不是使用http协议进行数据传输。所以浏览器的SOP无法限制它。而且websocket本来就是设计成支持跨域访问的协议的。在websocket请求的请求头中会像cors一样加入origin字段,服务端可以根据这个字段来判断是否通过该请求。
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| const WebSocket = require('ws') const ws = new WebSocket.Server({ port: 3888 }, () => { console.log('socket start') }) ws.on('connection', (client) => { client.send('连接成功') client.on('message', (msg) => { const { name, age } = JSON.parse(msg.toString()) client.send(`我叫${name},今年${age}岁`) }) client.on('close', (msg) => { console.log('关闭服务器连接') }) })
<button onclick="send()">发送消息</button>
const ws = new WebSocket("ws://localhost:3888/") ws.onopen = function(){ console.log("服务器连接") } ws.onmessage= (msg)=>{ console.log(msg.data) } ws.onclose=()=>{ console.log("服务器关闭") } function send(){ let msg = { name: '笨鸟', age: 22 } ws.send(JSON.stringify(msg)) }
|

4. postMessage
postMessage() 方法允许来自不同源的脚本采用异步方式进行有效的通信,可以实现跨文本文档,多窗口,跨域消息传递。多用于窗口间数据通信,这也使它成为跨域通信的一种有效的解决方案。
简而言之,可以跨域实现两个网页间的通讯
我在网上搜到关于postMessage原理和应用场景写的比较好的文章
注意点
- 首先,信息传递安全问题。为了你的信息传递能准确传达,无论是作为主页面还是子页面,传递重要信息时都应该填写 origin 而不是 “*”,避免被截获。
- 其次,iframe 或者 window.open 的 load 事件是不准确的。为了避免漏发漏接的情况,建议在通讯页面里回传加载状态。
a页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <div id="app"> 我是a页面 <div id="recMessage"></div> </div>
window.onload = function() { var messageEle = document.getElementById('recMessage'); window.addEventListener('message', function (e) { if (e.origin !== "http://localhost:8848") { return; } messageEle.innerHTML = "从"+ e.origin +"收到消息: " + e.data; }); }
|
b页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <div id="app"> 我是b页面 <iframe id="receiver" src="http://localhost:8099/a.html" frameborder="0"></iframe> <button id="sendMessage">发送信息</button> </div>
window.onload = function() { var receiver = document.getElementById('receiver').contentWindow; var btn = document.getElementById('sendMessage'); btn.addEventListener('click', function (e) { e.preventDefault(); receiver.postMessage("Hello!我是来自8848端口的b页面",'http://localhost:8099'); }); }
|

5. node中间件代理
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。
- 接受客户端请求 。
- 将请求 转发给服务器。
- 拿到服务器 响应 数据。
- 将 响应 转发给客户端。

1) 非vue框架的跨域
koa2 + node + koa-server-http-proxy
koa-server-http-proxy相当于http-proxy-middleware的koa版本
前端
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var xmlhttp; if (window.XMLHttpRequest) { xmlhttp=new XMLHttpRequest(); } else { xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); } xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState==4 && xmlhttp.status==200) { console.log(xmlhttp.responseText) } } xmlhttp.open("GET","http://localhost:3000/string",true); xmlhttp.send();
|
后端1
1 2 3 4 5 6 7 8 9
| const proxy = require('koa-better-http-proxy') const cors = require('koa-cors') app.use(cors()) app.use( proxy('127.0.0.1', { port: 3001, }) )
|
后端2
1 2 3 4 5
| router.get('/string', async (ctx, next) => { ctx.body = `我叫笨鸟` })
|
2) vue框架的跨域
平时我们用脚手架搭建的vue项目,为啥在webpack.config.js配置文件配置proxy参数就可以实现代理?
当你运行项目的时候,会配置启动一个node
服务,这个服务的作用1是静态文件服务,让你可以访问到html/js
等文件包括监听文件变动等;2是启动一个http代理,你js发送的请求会请求到这个服务A,由服务A代理到服务B,而服务A和静态文件服务器是同源的,并不影响同源策略。
脚手架的代理
vue-cli
的proxyTable
用的是http-proxy-middleware
中间件
create-react-app
用的是webpack-dev-server
内部也是用的http-proxy-middleware
webpack.config.js部分配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| module.exports = { entry: {}, module: {}, ... devServer: { historyApiFallback: true, proxy: [{ context: '/login', target: 'http://www.domain2.com:8080', changeOrigin: true, secure: false, cookieDomainRewrite: 'www.domain1.com' }], noInfo: true } }
|
6. nginx 反向代理
实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
nginx配置(nginx -s reload
启动nginx)
1 2 3 4 5 6 7
| server { listen 8888; server_name localhost; location / { proxy_pass http://localhost:3001; } }
|
后端
1 2 3 4
| router.get('/string', async (ctx, next) => { ctx.body = `我叫笨鸟` })
|
前端
1 2 3 4 5 6 7 8 9 10 11 12 13
| var xmlhttp; if (window.XMLHttpRequest) { xmlhttp=new XMLHttpRequest(); } else { xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); } xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState==4 && xmlhttp.status==200) { console.log(xmlhttp.responseText) } } xmlhttp.open("GET","http://localhost:8888/string",true); xmlhttp.send();
|
总结
- CORS 支持所有类型的 HTTP 请求,是跨域 HTTP 请求的根本解决方案
- JSONP 只支持 GET 请求,JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
- 不管是 Node 中间件代理还是 nginx 反向代理,主要是通过同源策略对服务器不加限制。
- 日常工作中,用得比较多的跨域方案是 cors 和 nginx 反向代理