前端跨域问题
浏览器的同源策略
提到跨域不能不先说一下”同源策略”。
何为同源?只有当协议、端口、和域名都相同的页面,则两个页面具有相同的源。只要网站的 协议名protocol、 主机host、 端口号port 这三个中的任意一个不同,网站间的数据请求与传输便构成了跨域调用,会受到同源策略的限制。
同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。浏览器的同源策略,出于防范跨站脚本的攻击,禁止客户端脚本(如 JavaScript)对不同域的服务进行跨站调用(通常指使用XMLHttpRequest请求)。
跨域请求方式
解决跨域问题,最简单的莫过于通过nginx反向代理
进行实现,但是其需要在运维层面修改,且有可能请求的资源并不再我们控制范围内(第三方),所以该方式不能作为通用的解决方案,下面阐述了经常用到几种跨域方式:
首先,先设置一下环境来调试跨域问题
1、利用node.js作为服务端,关于搭建就用 node express 快速搭建
<figure style="margin: 1em 0px;"> image<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">生成一个测试应用demo</figcaption>
</figure>
2、本地配置域名
打开hosts,授予管理权限并打开编辑,加入
127.0.0.1 www.mynodetest.com
127.0.0.1 child.mynodetest.com
打开终端,cd进入myapp目录,执行
node start
在publish文件中创建index.html文件
<figure style="margin: 1em 0px;"> image</figure>
然后打开浏览器,执行 www.mytestnode.com/index.html 就能访问 index.html页面
<figure style="margin: 1em 0px;"> image如果网页提示域名解析错误,那么就是本地网络设置了固定ip,dns,代理的问题
① 将internet协议版本如(TCP/IPv4)中的固定值设置为自动获取,同时取消代理
</figure>
<figure style="margin: 1.6em 0px 1em;"> image</figure>
<figure style="margin: 1em 0px;"> image② 代理中直接加入过滤 mynodetest.com
</figure>
基本调试环境设置完毕,开始尝试跨域方式
一、jsonp (只能支持get请求)
打开 myapp/routes/index.js,加入一个jsonp请求
<figure style="margin: 1em 0px;"> image</figure>
同时在html也加入一个script
<figure style="margin: 1em 0px;"> image</figure>
刷新浏览器,地址还是 www.mynodetest.com/index.html
<figure style="margin: 1em 0px;"> image</figure>
二、document.domain + iframe
仅限主域相同,子域不同的应用场景
浏览器访问的地址是 http://www.mynodetest.com:3000/index.html
嵌入iframe的地址是 http://child.mynodetest.com:3000/test.html
都用js手动设置 document.domain = 'mynodetest.com'; 作为基础域,继而实现同域
myapp/publish 新增test.html, 并加入以下代码
<figure style="margin: 1em 0px;"> image</figure>
index.html中加入 user, 并使用iframe嵌入 http://child.mynodetest.com:3000/test.html
<figure style="margin: 1em 0px;"> image</figure>
好了,此时重新刷新页面,在控制台能看见跨域的问题了,
<figure style="margin: 1em 0px;"> image</figure>
把test.html中注释的地方取消注释,在刷新页面
<figure style="margin: 1em 0px;"> image</figure>
就能看到在index.html中定义的 user
三、location.has + iframe
a 与 b 不同域,但是相互之间要进行通信,可以通过中间页 c 与(a 同域)实现,不同域使用hash传值,相同域使用js访问
1、 a.html (http://www.mynodetest.com:3000/a.html)
<body>
<iframe id='iframe' src="http://child.mynodetest.com:3000/b.html" frameborder="0"></iframe>
<button id='button'>set b.html #user</button>
</body>
<script>
document.getElementById('button').onclick = () => {
var iframe = document.getElementById('iframe');
iframe.src = `${iframe.src}#user=mynodetest`;
}
function cb(result) {
console.log(result);
}
</script>
2、b.html (http://child.mynodetest.com:3000/b.html)
<body>
<iframe id='iframe' src="http://www.mynodetest.com:3000/c.html" frameborder="0"></iframe>
</body>
<script>
var iframe = document.getElementById('iframe');
window.onhashchange = () => {
iframe.src = `${iframe.src}${location.hash}`;
}
</script>
3、c.html (http://www.mynodetest.com:3000/c.html)
<script>
window.onhashchange = () => {
window.parent.parent.cb(location.hash.replace('#user=', ''));
}
</script>
四、window.name + iframe
window.name 在不用的地址(甚至不同域名)加载后依旧存在于该窗口,上限 2M
通过 iframe.src 外域转向本地域,跨域数据由 iframe 的 window.name 从外域传到本地域,绕过浏览器的访问限制,同时又是安全操作
1、a.html (http://www.mynodetest.com:3000/a.html)
const proxy = (url, cb) => {
let state = 0;
let iframe = document.createElement('iframe');
iframe.src = url;
iframe.onload = () => {
if(state === 0) {
iframe.contentWindow.location = 'http://www.mynodetest.com:3000/c.html';
state = 1;
} else if(state === 1) {
cb(iframe.contentWindow.name);
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
}
document.body.appendChild(iframe);
}
proxy('http://child.mynodetest.com:3000/b.html', name => console.log(name));
2、c.html (http://www.mynodetest.com:3000/c.html)
没什么内容,主要的作用是与 a 作用域相同
3、b.html (http://child.mynodetest.com:3000/b.html)
window.name = 'b.html'
五、postMessage
postMessage是 HTML5 XMLHttpRequest Level 2中的API, 支持浏览器跨域操作
用法 postMessage( data, origin )
data:html5 规范支持任意基本类型或可复制对象,但部分浏览器只支持字符串,可用 JSON.stringify() 序列化。
origin:协议 + 主机 + 端口号,可以设置为 "*",表示可以传递给任意窗口,要指定和当前窗口同源设置为 "/"。
- 页面和其它打开的新窗口的数据传递
- 多窗口之间的信息传递
- 页面与嵌套iframe消息传递
- 以上三个场景的跨域数据传递
⑴ a.html (http://www.mynodetest.com:3000/a.html)
<iframe id="iframe" src="http://child.mynodetest.com:3000/b.html"></iframe>
let iframe = document.getElementById('iframe');
iframe.onload = () => {
const data = { name: 'a to b' };
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://child.mynodetest.com');
}
window.addEventListener('message', e => {
console.log(e.data);
})
⑵ b.html (http://child.mynodetest.com:3000/b.html)
window.addEventListener('message', e => {
console.log(e.data);
window.parent.postMessage('b to a ', 'http://www.mynodetest.com');
})
六、跨域资源共享 (CORS)
普通跨域请求: 只需要服务端设置 Access-Control-Allow-Origin
带cookie请求:前后端都需要设置值,所带的cookie为跨域请求接口所在域的cookie
1、前端请求
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('post', 'http://child.mynodetest.com:3000/post', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}
2、node (express)
router.post('/post', function(req, res) {
//req.headers.origin
res.header("Access-Control-Allow-Origin", 'http://www.mynodetest.com:3000'); //需要显示设置来源
//res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
//res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("Access-Control-Allow-Credentials",true); //带cookies
//res.header("X-Powered-By",' 3.2.1')
//res.header("Content-Type", "application/json;charset=utf-8");
res.header('Set-Cookie', 'l=a123456;Path=/;Domain=child.mynodetest.com;HttpOnly');
res.json({ test: '22222' });
});
七、nginx代理跨域
1、nginx配置解决iconfont跨域
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,可在nginx的静态资源服务器中加入以下配置。
location / { add_header Access-Control-Allow-Origin *; }
2、nginx反向代理接口跨域
跨域原理:同源策略是浏览器的安全策略,不是http协议的一部分。服务端调用http接口只是http协议,不会执行js脚本,不需要同源策略,也不存在跨域问题。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同),反向代理访问domain2接口,可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域
假设当前有两个域名 www.domain1.com www.domain2.com
#proxy服务器
server {
listen 81;
server_name www.my;
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;
}
}
⑴ 前端
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
⑵ node
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...');
八、node中间件代理
与nginx相同,通过启动一个代理服务器,实时数据转发
1、前端
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
2、node中间件
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目标接口
target: 'http://www.domain2.com:8080',
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改响应信息中的cookie域名
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');
九、websocket协议
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。原生WebSocket API使用起来不太方便,我们使用http://Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
1、前端
<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
2、node socket
var http = require('http');
var socket = require('socket.io');
// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
网友评论