前端开发的历史年轮
- 服务端渲染
- 客户端渲染(同源策略)
- 客户端渲染(跨域方案)
- 半服务端渲染(SSR)
谈谈你对跨域的理解
跨域主要分3部分:
- 协议相同
- 域名相同
- 端口相同
只要有一个不同,那么就是跨域
// 地址
http://www.baidu.com
协议:http://
域名:www.baidu.com
端口:8080(http) 443(https) 默认端口省略
http://www.baidu.com/login.html // 同源
http://www.baidu2.com/login.html // 不同源,域名不同
http://www.baidu.com:81/login.html // 不同源,端口不同
同源的目的
目的是为了保护用户信息的安全,防止恶意网站窃取数据,否则Cookie可以共享。有的网站一般会把一些重要信息存放在cookie或者LocalStorage中,这时如果别的网站能够获取获取到这个数据,可想而知,这样就没有什么安全可言了。
限制范围
- Cookie、LocalStorage和IndexDB 无法读取
- DOM无法获得
- AJAX 请求不能发送
主要这3种方式不行。
解决方案
方案1:CORS
比较常见的就是nodejs配置CORS允许跨域。
- Access-Control-Allow-Origin
- 字段必传,为*表示允许任意域名的请求。当有cookie需要传递时,需要指定域名。
- Access-Control-Allow-Credentials
- 字段可选,默认为false,表示是否允许发送cookie。若允许,通知浏览器也要开启cookie值的传递。
- Access-Control-Expose-Headers
- 字段可选。如果想要浏览器拿到getResponesHeader()其他字段,就在这里指定。
- Access-Control-Request-Method
- 必须字段,非简单请求时设置的字段,例如PUT请求。
- Access-Control-Request-Headers
- 指定额外的发送头信息,以逗号分割字符串。
module.exports = {
//=>WEB服务端口号
PORT: 3001,
//=>CROS跨域相关信息
CROS: {
ALLOW_ORIGIN: 'http://127.0.0.1:5500',
ALLOW_METHODS: 'PUT,POST,GET,DELETE,OPTIONS,HEAD',
HEADERS: 'Content-Type,Content-Length,Authorization, Accept,X-Requested-With',
CREDENTIALS: true
}
};
app.use((req, res, next) => {
const {
ALLOW_ORIGIN,
CREDENTIALS,
HEADERS,
ALLOW_METHODS
} = CONFIG.CROS;
res.header("Access-Control-Allow-Origin", ALLOW_ORIGIN);
res.header("Access-Control-Allow-Credentials", CREDENTIALS);
res.header("Access-Control-Allow-Headers", HEADERS);
res.header("Access-Control-Allow-Methods", ALLOW_METHODS);
req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next();
});
方案2:Proxy
现在主流三大框架,react,vue,argular都使用了webpack进行工程化。在本地开发最常见的就是proxy代理,解决跨域。
主要原理是:客户端像服务器请求数据。webpack-dev-server会再本地创建一个web服务,这个服务会和客户端同源。本地服务实际上是一个node服务,它作为一个中间层会帮客户端去像服务端请求数据,然后把数据返回给客户端。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/main.js',
output: {
filename: 'main.[hash].min.js',
path: path.resolve(__dirname, 'build')
},
devServer: {
port: '3000',
compress: true,
open: true,
hot: true,
proxy: {
'/': {
target: 'http://127.0.0.1:3001',
changeOrigin: true
}
}
},
// 配置WEBPACK的插件
plugins: [
new HtmlWebpackPlugin({
template: `./public/index.html`,
filename: `index.html`
})
]
};
方案3:JSONP
主要原理:link,script这种是不会跨域的。所以,前端代码写一个script src = http://localhoost:80/list?callback=func,把这个链接发送给服务端。但是传递给服务端的函数必须是一个全局的函数。服务端接受到请求后,会把callback这个值,返回给客户端。客户端获取到服务端返回的指定格式字符串。发现其实就是本地的func全局函数执行,并且把数据传递给这个函数。
但是这种方式有一个弊端,那就是只能get请求,而且不安全,只要服务端支持,谁都可以调用。
下面手写一个JSONP的实现
function jsonp(url = "", callback) {
let script;
// 把传递的回调函数挂载到全局上
let name = `jsonp${new Date().getTime()}`;
window[name] = data => {
// 从服务器获取到了结果
document.body.removeChild(script);
delete window[name];
callback && callback(data);
};
// 处理URL
url += `${url.includes('?') ? '&' : '?'}callback=${name}`;
// 发送请求
script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
}
jsonp('http://127.0.0.1:1001/list?lx=1', result => {
console.log(result);
});
jsonp('https://matchweb.sports.qq.com/matchUnion/cateColumns?from=pc', result => {
console.log(result);
});
方案4:nginx反向代理
这是后端需要做的,其实我也不是很熟悉,大致配置方式。
server {
listen 80;
server_name 192.168.161.189;
#charset koi8-r;
#access_log logs/host.access.log main ;
location {
proxy_pass http: // 192.168.161.189:8070;
root html;
index index.html index.html;
}
}
什么是代理?
既然是代理跨域,那么代理(Proxy Server)就是一个很重要的点,这里的代理说的服务器代理,是一种很重要的服务器安全功能,也是一种很常见的设计模式,来隔绝不同的模块,解耦模块。
为什么代理是反理?
nginx就能够把用户的请求分发到空闲的服务器上,然后服务器返回自己的服务到负载均衡设备上,然后负载均衡的设备会讲服务器的服务返回给用户,所以我们并不知道为什么服务的是哪一台服务器发送出来的,这就很好的隐藏了服务器。有一句精辟的话是这么说的:“反向代理就是流量发散状,代理是流量汇聚状。”
方案5:POST MESSAGE
A.html
<iframe id="iframe" src="http://127.0.0.1:1002/B.html" frameborder="0" style="display: none;"></iframe>
iframe.onload = function () {
iframe.contentWindow.postMessage('消息', 'http://127.0.0.1:1002/');
}
//=>监听B传递的信息
window.onmessage = function (ev) {
console.log(ev.data);
}
B.html
window.onmessage = function (ev) {
// console.log(ev.data);
//=>ev.source:A
ev.source.postMessage(ev.data + '@@@', '*');
}
方案6:基于iframe的跨域解决方案1——locaction.hash
原理:也是利用iframe可以在不同域中传值的特点,而location.hash正好可以携带参数,所以利用iframe作为这个不同域之间的桥梁。
A域名页面
var iframe = document.createElement('iframe')
iframe.src = 'http://www.B.com:80/hash.html'
document.body.appendChild(iframe)
window.onhashchange = function () {
//处理hash
console.log(location.hash)
}
B域名页面
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var res = JSON.parse(xhr.responseText)
console.log(res.msg)
parent.location.href = `http://www.A.com:80/a.html#msg=${res.msg}`
}
}
xhr.open('GET', 'http://www.B.com:80/json', true)
xhr.send(null)
缺点
- iframe虽然能解决问题,但是安全风险还是比较重要的。
- hash传参处理起来比较麻烦。
方案7:基于iframe的跨域解决方案2——window.name
原理其实是和上面的方法一样,区别在于window.name能够传递2MB以上的数据。
A域名页面
var iframe = document.createElement('iframe')
iframe.src = 'http://www.B.com:80/name.html'
document.body.appendChild(iframe)
var times = 0
iframe.onload = function () {
if (times === 1) {
console.log(JSON.parse(iframe.contentWindow.name))
destoryFrame()
} else if (times === 0) {
times = 1
}
}
// 获取数据以后销毁这个iframe,释放内存;
function destoryFrame() {
document.body.removeChild(iframe);
}
B域名页面
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
window.name = xhr.responseText
location.href = 'http://www.A.com:80/a.html'
}
}
xhr.open('GET', 'http://www.B.com:80/json', true)
xhr.send(null)
等等其他处理方式