每日一句
We get to decide what our story is.
释义:我们的故事由我们自己决定。
跨域是个啥,为什么会有跨域?
前言:在前端和后端交互过程中,经常会遇到跨域的情况,那这个跨域是什么意思?
概念理解
那我们来解释下跨域:
这是受到浏览器同源策略的影响,如果没有同源策略,浏览器很容易遭受一些攻击像xss, csrf等等。
同源策略是指协议+域名+端口都一致的情况叫同源,其他任意一种情况不同都是非同源或不同域。不同域之间访问资源就叫"跨域"。
同源策略限制了哪些内容?
- cookie,localStorage, indexDB
- dom
- ajax
有些标签元素不受同源影响可以访问资源:img,script,link
很多初级开发看到了报跨域错误时,会认为请求没有发出去,这是错误的理解。
其实请求是发出去,后端服务器中可以看到日志正常返回了数据,只不过是被浏览器认为这是不安全的访问给拦截了而矣!
跨域的几种情况
由上面的概念不难看出,跨域的几种情况:
- 协议不同
https://demo.com 与 http://demo.com - 域名不同
https://demo.com 与 http://www.demo.com - 端口不同
https://demo.com 与 https://demo.com:81 - ip与域名不同
https://11.22.11.22 与 https://demo.com
解决跨域方法
1.script方式jsonp
因为script不受同源影响,可利用其封装一个jsonp方法
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script');
window[callback] = function(data) {
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback }
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
jsonp({ url: 'http://localhost:888/api', params: { toEnd: '利用jsonp获取数据' }, callback: 'say' }).then(data => {
console.log(data)
})
服务器端简易代码:
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
let { toEnd, callback } = req.query
res.end(`${callback}('利用jsonp获取数据')`)
})
app.listen(3000)
利用jquery的jsonp
$.ajax({
url:"http://demo.com/apiData",
dataType:"jsonp",
type:"get",//可省略
jsonpCallback:"say",//可省略
jsonp:"callback",//可省略
success:function (data){
console.log(data);
}
});
其他 $.getJSON(数据量小), $.post
2. iframe
- document.domain
原理:参与传递数据的页面设置相同的域
如页面1: a.demo.com/a.html, 页面2: b.demo.com/b.html, 则把页面1和页面2都设置document.domain="demo.com".
- document.location
原理:URL带hash, 通过一个非跨域的中间页来实现数据传递
一开始 a.html 给 c.html 传一个 hash 值,然后 c.html 收到 hash 值后,再把 hash 值传递给 b.html,最后 b.html 将结果放到 a.html 的 hash 值中。 同样的,a.html 和 b.htm l 是同域的,都是 http://localhost:8000,而 c.html 是http://localhost:8080
- document.name
window 对象的 name 属性是一个很特别的属性,当该 window 的 location 变化,然后重新加载,它的 name 属性可以依然保持不变。
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
// a.html(http://localhost:8081/b.html)
<iframe src="http://localhost:8082/c.html" frameborder="0" onload="load()" id="ifr"></iframe>
<script>
let first = true
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
function load() {
if(first){
// 第1次onload(跨域页)成功后,切换到同域代理页面
let ifr = document.getElementById('ifr');
ifr.src = 'http://localhost:8081/b.html';
first = false;
}else{
// 第2次onload(同域b.html页)成功后,读取同域window.name中数据
console.log(ifr.contentWindow.name);
}
} </script>
// c.html(http://localhost:8082/c.html)
<script> window.name = '我的name不会变' </script>
通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
3. cors
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)跨域资源共享 CORS 详解。这是跨域问题的标准做法。
CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
CORS有两种请求,简单请求和非简单请求。
简单请求:get,post,head 之一, 且请求头是
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
复杂请求:会在正式请求之前,增加HTTP查询请求,称为"预检"请求(preflight),该请求是 option 方法,该请求来判断服务端是否允许跨域。
后台需要设置常用属性(需要根据实际情况按需设置)
- 带cookie 需要前后端都设置credentials,且后端设置指定的origin
ctx.set('Access-Control-Allow-Origin', 'http://localhost:8088')
ctx.set('Access-Control-Allow-Credentials', true)
- 设置Access-Control-Request-Method以及Access-Control-Request-Headers
ctx.set('Access-Control-Request-Method', 'PUT,POST,GET,DELETE,OPTIONS')
ctx.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, t')
ctx.cookies.set('tokenId', '2')
4.反向代理
1.nginx中
location ^~ /api {
proxy_pass http://localhost:8088;
}
2.vue.config.js配置
devserver {
proxy: {
'/api': {
target: 'http://10.10.137.255:8081',
changeOrigin: true,
ws: true,
pathRewrite: {
'^/api': ''
}
}
}
}
5.websocket
[WebSocket] 规范定义了一种 API,可在网络浏览器和服务器之间建立“套接字”连接。简单地说:客户端和服务器之间存在持久的连接,而且双方都可以随时开始发送数据。
前端代码
<script>
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function() {
scolet.send('前端发送的数据111')
}
socket.onmessage = function(data) {
console.log(data)
}
</script>
后端代码
let app = require('express')()
let WebSocket = require('ws')
let wss = new WebSocket.Server({port:3000});
wss.on('connection', function(ws) {
ws.on('message', function(data) {
console.log(data)
ws.send('后端发送的数据222')
})
})
6.Node中间件
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。
部分代码:
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// 拦截http://localhost:5000/admin/的请求,转到目标服务器:http://localhost:8081/admin/
app.use('/admin/*', createProxyMiddleware({ target: 'http://localhost:8081', changeOrigin: true }));
//配置服务端口
app.listen(5000, () => { console.log(`localhost:5000`); });
7.postMessage
1.使用场景
- 与iframe里的页面传递数据
- 多窗口之间数据传递
- 与打开的新窗口传递数据
2.用法
otherWindow.postMessage(message, targetOrigin, [transfer]);
3.示例
// index.html
<iframe src="http://localhost:8080/a.html" frameborder="0" id="ifr" onload="load()"></iframe>
<script>
function load() {
ifr.contentWindow.postmessage('我是index', 'http://localhost:8080')
window.onmessage = function(e) { console.log(e.data) }
}
</script>
// a.html
<script>
window.onmessage = e => {
e.source.postMessage(e.data, e.origin);
}
</script>
8.canvas操作图片的跨域问题
张大神解决了canvas图片getImageData,toDataURL跨域问题
总结
cors和反向代理是日常开发用的比较多的方案,如果你还在兼容老的浏览器可以用jsonp方式
浏览器开启跨域方式,此方法可以从源头关闭浏览器的同源策略(此方法不建议)
- window
找到你安装的目录
.\Google\Chrome\Application\chrome.exe --disable-web-security --user-data-dir=xxxx
- Mac
/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --disable-web-security --user-data-dir=~/Downloads/chrome-data
~/Downloads/chrome-data 这个目录可以自定义.
当然在开发中使用ajax跨域时,还可以利用chrome插件Allow CORS: Access-Control-Allow-Origin来实现跨域,用起来也是屡试不爽。