js跨域
1.同源策略
同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制内容有:
Cookie、LocalStorage、IndexedDB 等存储性内容
DOM 节点
AJAX 请求发送后,结果被浏览器拦截了
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。
2.常见跨域场景
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示:

3. 实现跨域的九种方式
jsonp
有几个标签是允许跨域加载资源:
<img src=XXX>
<link href=XXX>
<script src=XXX>
<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链
JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击(jsonp请求返回<script></script>)。
<script>
function kimi(data) {
console.log(data)
}
</script>
<script src="https://www.baidu.com/sugrec?prod=pc&wd=8&cb=kimi">
</script>
cors
浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
服务端设置了Access-Control-Allow-Origin就开启了CORS通信,表示允许哪个域来访问我。
具体需要设置的头部有很多,列举常用的:
// 设置哪个源可以访问我
res.setHeader('Access-Control-Allow-Origin', origin)
// 允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Headers', 'name')
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name')
postMessage
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
// a.html
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe>
<script>
function load() {
let frame = document.getElementById('frame')
frame.contentWindow.postMessage('kimi', 'http://localhost:4000') //发送数据
window.onmessage = function(e) { //接受返回数据
console.log(e.data) //yes
}
}
</script>
// b.html
window.onmessage = function(e) {
console.log(e.data) //kimi
e.source.postMessage('yes, e.origin)
}
http-proxy
一般在使用webpack时webpack-dev-server会加proxy-table来转发请求。因为同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略
nginx
实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。
// nginx proxy服务器
server {
listen 81;
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;
}
}
websocket
websocket 是没有跨域限制的。
// socket.html
<script>
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
socket.send('kimi1');//向服务器发送数据
}
socket.onmessage = function (e) {
console.log(e.data);//接收服务器返回的数据
}
</script>
// server.js
let express = require('express');
let app = 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('kimi2')
});
})
document.domain
该方式只能用于二级域名相同的情况下,比如 video.baidu.com 和 www.baidu.com 适用于该方式。
// a.html
<body>
helloa
<iframe src="http://video.baidu.com" frameborder="0" onload="load()" id="frame"></iframe>
<script>
document.domain = 'baidu.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script>
</body>
// b.html
<body>
hellob
<script>
document.domain = 'baidu.cn'
var a = 100;
</script>
</body>
window.name
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
实现:先在a页面中嵌入c页面,在c页面中写入window.name值, c加载完毕后马上切换成a同源的b页面。等b加载完后可以拿到没有清除的name值
// a.html(http://localhost:3000/b.html)
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
function load() {
if(first){
// 第1次onload(跨域页)成功后,切换到同域代理页面
let iframe = document.getElementById('iframe');
iframe.src = 'http://localhost:3000/b.html';
first = false;
}else{
// 第2次onload(同域b.html页)成功后,读取同域window.name中数据
console.log(iframe.contentWindow.name);
}
}
</script>
// c.html(http://localhost:4000/c.html)
<script>
window.name = 'yes'
</script>
location.hash
a页面嵌入c页面带给他hash数据,c页面中拿到后,再嵌入和a同源的b页面带入hash数据,b拿到后再设置回a页面的hash中。a中再监听hash变化来获取。
// a.html
<iframe src="http://localhost:4000/c.html#kimi"></iframe>
<script>
window.onhashchange = function () { //检测hash的变化
console.log(location.hash);
}
</script>
// b.html
<script>
window.parent.parent.location.hash = location.hash
//b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
</script>
// c.html
console.log(location.hash);
let iframe = document.createElement('iframe');
iframe.src = 'http://localhost:3000/b.html#yes';
document.body.appendChild(iframe);
总结
现在业界主流使用解决跨域问题的方案主要是 nginx和cors、jsonp(优势在于可以兼容老版本浏览器,但是只支持get并且不安全),本地开发常用node-proxy的方式做反向代理。其他的方式都不常用。location.hash和window.name都需要有第三方中介。而domain的方式会有域名的限制条件。
网友评论