浏览器的同源策略
# 浏览器的同源策略
# 介绍一下什么是同源策略特别重要
同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议、端口号、域名必须一致
同源策略主要限制了
当前域下的
js
不能访问到其他域下的cookies
、localstorage
和indexDB
当前域不能操作和访问其他域下的
DOM
当前域下的
ajax
无法发送跨域请求
同源策略主要是保证用户的信息安全,只是对js
脚本的一种限制,并不是对浏览器的限制,对于一般的img
都不会有跨域的限制,因为这些都不会出现安全问题
值得注意的是
<script>
里面的src
指向的请求链接也不会🈶️跨域的限制,参考JSONP的实现
# 如何解决跨域问题
CORS
概念
跨域资源共享(CORS
) 是一种机制,它使用额外的 HTTP
头来告诉浏览器 让运行在一个 origin
(domain
)上的Web
应用被准许访问来自不同源服务器上的指定的资源
简单地来说就是只要服务器端实现了cors
请求就可以跨源通信了
浏览器将cors
分为简单请求和非简单请求
简单请求不会触发cors
预检请求
HTTP
请求方法是HEAD
GET
POST
HTTP
请求头信息不超过以下的字段Accept
、Accept-Language
、Content-Language
、Last-Event-ID
、Content-Type
:只限于三个值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
值得注意的是
比较常见的json
就不是简单请求了
head请求
请求资源的头部信息, 并且这些头部与HTTP
GET
方法请求时返回的一致. HEAD
方法的响应不应包含响应正文.
通常用于
下载一个大文件前先获取其大小再决定是否要下载, 以此可以节约带宽资源
检查文件是否有最新版本,服务器会在响应头里把文件的修改时间传回来。
# 为什么会出现简单请求和非简单请求
跨域只是浏览器的一种保护机制,是为了保护站点的安全,其实跨域本身是不存在的,为了安全浏览器针对XMLHTTPRequest
和fetch
这种脚本发起的跨域请求做了一定的跨域限制,而对于浏览器自身的如img
的src
、script
的js
资源、form
表单的提交,没有做跨域的限制。
简单请求:这是因为img
的src
、script
的js
只能发起get
请求,而表单虽然能进行post
提交,但一方面表单的提交是显式的,用户可以感知,另一方面,表单提交只能发起请求,而不能获取请求的响应,这样一来,请求可以发起,而服务端可以进行拒绝,浏览器认为这是安全的。
复杂请求:通过脚本发起的跨域请求,可以对响应内容做处理,这是用户不可感知的,浏览器认为这是不安全的,所以对于复杂请求,进行跨域的限制,而CORS
机制就是浏览器对跨域进行处理。对于复杂请求会发起一个预检请求,判断服务端是否可以接受这个跨域请求,接受后就可以向服务端发起真正的请求。
# 简述一下简单请求的过程特别重要
浏览器会直接发出CORS
请求,他会在他会在请求的头信息上增加一个Origin
字段,该字段用来说明本次请求来自于哪一个源(协议+端口+域名),如果origin
指定的域名在许可范围内,服务器返回的响应会多出以下信息
Access-Control-Allow-Origin: http://api.bob.com // 和Orign一直
Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie
Access-Control-Expose-Headers: FooBar // 指定返回其他字段的值
Content-Type: text/html; charset=utf-8 // 表示文档类型
2
3
4
如果指定的域名不在许可范围内,服务器会返回一个正常的HTTP
回应,浏览器发现没有上面的Access-Control-Allow-Origin
,表示就出错了,这个是无法通过状态码进行判断的
# 简述一下非简单请求的过程特别重要
非简单请求比如DELETE
PUT
会在正式通信之前会进行一次预检请求,就是当前所在的网页是否在服务器允许访问的范围内,以及可以使用的请求方法,只有得到服务端肯定的回复才能进行正式的HTTP
请求,否则就会报错
预检的请求方法是option
,他的头信息关键就是origin
,除此之外还有
Access-Control-Request-Method
:该字段是必须的,用来列出浏览器的CORS
请求会用到哪些HTTP
方法。Access-Control-Request-Headers
: 该字段是一个逗号分隔的字符串,指定浏览器CORS
请求会额外发送的头信息字段
如果服务器根据头信息的三个字段进行判断,如果返回的头信息有Access-Control-Allow-Origin
就是允许跨域请求
服务器返回的cors
字段有
Access-Control-Allow-Origin: http://api.bob.com // 允许跨域的源地址
Access-Control-Allow-Methods: GET, POST, PUT // 服务器支持的所有跨域请求的方法
Access-Control-Allow-Headers: X-Custom-Header // 服务器支持的所有头信息字段
Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie
Access-Control-Max-Age: 1728000 // 用来指定本次预检请求的有效期,单位为秒
2
3
4
5
前三个是服务端是必须返回的字段
值得注意的是我们在日常的开发中要减少options请求次数,因为
- 请求次数过多会损耗页面的加载性能,降低用户体验度
- 可以在后端返回头部添加
Access-Control-Max-Age
表示可以被缓存多久,单位是秒,对完全一样的URL
缓存设置生效,在这个时间段内就不需要预检了
还有一个是CORS
中的cookies
问题
在CORS
请求中如果想传递cookies
,要满足以下三个条件:
- 在请求中加上
withCredentials
// 原生 xml 的设置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// axios 设置方式
axios.defaults.withCredentials = true;
2
3
4
5
Access-Control-Allow-Credentials
设置为true
Access-Control-Allow-Origin
设置为非*
# 其他跨域方式
# JSONP
原理
利用<script>
标签没有跨域限制,通过<script>
标签src
属性,发送带有callback
参数的GET
请求,服务端将接口返回数据拼凑到callback
函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback
函数返回的数据。
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.crucials:80/login?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>
2
3
4
5
6
7
8
9
10
11
服务端返回如下(返回时即执行全局函数)
handleCallback({"success": true, "user": "admin"})
Vue axios
实现
this.$http = axios;
this.$http.jsonp('http://www.crucials:80/login', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
2
3
4
5
6
7
nodejs
代码
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
var params = querystring.parse(req.url.split('?')[1]);
var fn = params.callback;
// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('80');
console.log('Server is running at port 80...');
2
3
4
5
6
7
8
9
10
11
12
13
当然缺点就是仅支持get
方法和不安全,可能遭到xss
攻击
# postMessage
跨域
主要解决了
页面和其打开的新窗口的数据传递
多窗口之间的消息传递
页面与嵌套的
iframe
消息传递
用法:postMessage(data,origin)
方法接受两个参数:
data
:html5
规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()
序列化。origin
: 协议+主机+端口号,也可以设置为"*"
,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"
。
1)a.html:(a.com/a.html)
<iframe id="iframe" src="http://www.b.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向b传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.b.com');
};
// 接受b返回数据
window.addEventListener('message', function(e) {
alert('data from b ---> ' + e.data);
}, false);
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2)b.html:(b.com/b.html)
<script>
// 接收a的数据
window.addEventListener('message', function(e) {
alert('data from a ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回a
window.parent.postMessage(JSON.stringify(data), 'http://www.a.com');
}
}, false);
</script>
2
3
4
5
6
7
8
9
10
11
12
# nginx
代理跨域
实际上与CORS
跨域原理一样,都是配置Access-Control-Allow-Origin
…等字段。
nginx
配置解决iconfont
跨域 浏览器跨域访问js
、css
、img
等常规静态资源被同源策略许可,但iconfont
字体文件(eot|otf|ttf|woff|svg
)例外,此时可在nginx
的静态资源服务器中加入以下配置
location / {
add_header Access-Control-Allow-Origin *;
}
2
3
nginx反向代理接口跨域 跨域问题
同源策略仅是针对浏览器的安全策略。服务器端调用HTTP
接口只是使用HTTP
协议,不需要同源策略,也就不存在跨域问题。
实现思路:通过Nginx
配置一个代理服务器域名与a
相同,端口不同)做跳板机,反向代理访问b
接口,并且可以顺便修改cookie
中domain
信息,方便当前域cookie
写入,实现跨域访问
#proxy服务器
server {
listen 81;
server_name www.a.com;
location / {
proxy_pass http://www.b.com:8080; #反向代理
proxy_cookie_domain www.b.com www.a.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.a.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# nodejs
中间件代理跨域
原理也与nginx
大致相同,都是通过代理然后进行转发,也可以通过设置cookiesDomainRewrite
参数修改响应头中cookies
中域名,实现当前域的cookies
写入,方便接口登陆认证
非vue
框架的跨域 使用node + express + http-proxy-middleware
搭建一个proxy
服务器
前端:
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.a.com:3000/login?user=admin', true);
xhr.send();
2
3
4
5
6
中间件服务器代码
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目标接口
target: 'http://www.b.com:8080',
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.a.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改响应信息中的cookie域名
cookieDomainRewrite: 'www.a.com' // 可以为false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# iframe
跨域
个感觉没那么重要
点击查看
# document.domain
+ iframe
跨域
仅限主域相同,子域不同的跨域应用场景,实现原理:两个页面都通过js强制设置document.domain
为基础主域,就实现了同域
1)父窗口:(domain.com/a.html
)
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
2
3
4
5
子窗口:(child.domain.com/a.html
)
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
console.log('get js data from parent ---> ' + window.parent.user);
</script>
2
3
4
5
# location.hash + iframe
跨域
实现原理:a
欲与b
跨域相互通信,通过中间页c
来实现。 三个页面,不同域之间利用iframe的location.hash
传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent
访问a页面所有对象
a.html:(www.a.com/a.html)
<iframe id="iframe" src="http://www.b.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
2)b.html:(www.b.com/b.html)
<iframe id="iframe" src="http://www.a.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
2
3
4
5
6
7
8
3)c.html:(www.a.com/c.html)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
2
3
4
5
6
7
# window.name + iframe
跨域
window.name
属性的独特之处:name
值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name
值(2MB
)
1)a.html:(a.com/a.html)
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.a.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.b.com/b.html', function(data){
alert(data);
});
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
2)proxy.html:(a.com/proxy.html)
中间代理页,与a.html
同域,内容为空即可。
3)b.html:(b.com/b.html)
<script>
window.name = 'This is b data!';
</script>
2
3
通过iframe
的src
属性由外域转向本地域,跨域数据即由iframe
的window.name
从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作
# WebSocket
协议跨域
这个感觉也不太重要的🦆
原生WebSocket API
使用起来不太方便,我们使用Socket.io
,它很好地封装了webSocket
接口,提供了更简单、灵活的接口,也对不支持webSocket
的浏览器提供了向下兼容
前端:
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.b.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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
node:
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.');
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 介绍一下正向代理和反向代理
正向代理
- 客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如修改浏览器配置
反向代理
- 服务器为了能够将工作负载分配到多个服务器来提高网站性能 (负载均衡)等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。 一般使用反向代理后,需要通过修改
DNS
让域名解析到代理服务器IP
,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了
- 服务器为了能够将工作负载分配到多个服务器来提高网站性能 (负载均衡)等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。 一般使用反向代理后,需要通过修改
区别:
正向代理和反向代理结构都是一样的,都是client-proxy-server
的结构,他们主要的区别是在于中间的proxy
是哪一方设置的,在正向代理中,proxy
是client
设置的,用来隐藏client
,而在反向代理中,proxy
是server
设置的,用于隐藏server
# 负载平衡的两种实现方式
- 一种是使用反向代理的方式,用户的请求都发送到反向代理服务上,然后由反向代理服务器来转发请求到真实的服务器上,以此来实现集群的负载平衡。
- 另一种是
DNS
的方式,DNS
可以用于在冗余的服务器上实现负载平衡。因为现在一般的大型网站使用多台服务器提供服务,因此一个域名可能会对应多个服务器地址。当用户向网站域名请求的时候,DNS
服务器返回这个域名所对应的服务器IP
地址的集合,但在每个回答中,会循环这些IP
地址的顺序,用户一般会选择排在前面的地址发送请求。以此将用户的请求均衡的分配到各个不同的服务器上,这样来实现负载均衡。这种方式有一个缺点就是,由于DNS
服务器中存在缓存,所以有可能一个服务器出现故障后,域名解析仍然返回的是那个IP
地址,就会造成访问的问题。
# 四层负载均衡和七层负载均衡的区别
实现原理:四层基于IP
+端口的方式进行路由转发,七层基于请求URL
地址的方式进行代理转发。
实现方式:四层通过报文中的IP
地址和端口,再加上负载均衡设备所采用的负载均衡算法,最终确定选择后端哪台下游服务器。
以TCP
为例,客户端向负载均衡发送SYN
请求建立第一次连接,通过配置的负载均衡算法选择一台后端服务器,并且将报文中的IP
地址信息修改为后台服务器的IP
地址信息,因此TCP
三次握手连接是与后端服务器直接建立起来的。
七层服务均衡在应用层选择服务器,只能先与负载均衡设备进行TCP
连接,然后负载均衡设备再与后端服务器建立另外一条TCP
连接通道。因此,七层设备在网络性能损耗会更多一些。
安全方面:四层容易遭受SYN flood
攻击,容易将垃圾流量转发到服务端,而七层会在负载均衡设备上进行过滤
四层负载均衡 | 七层负载均衡 | |
---|---|---|
实现原理 | 基于IP 和端口 | 基于虚拟URL 或主机IP |
实现限制 | 类似路由器 | 代理服务器 |
握手次数 | 1 | 2 |
复杂度 | 低 | 高 |
性能 | 无需解析内容 | 需要算法识别URL ,cookies ,HTTP head 等 |
安全性 | 低,容易被DDos 攻击 | 可防御SYN Flood |
额外功能 | 无 | 会话保持,图片压缩,防盗链 |
# nginx
概念及其工作原理
Nginx
是一款轻量级的 Web
服务器,也可以用于反向代理、负载平衡和 HTTP
缓存等。Nginx
使用异步事件驱动的方法来处理请求,是一款面向性能设计的 HTTP
服务器。
传统的 Web
服务器如 Apache
是 process-based
模型的,而 Nginx
是基于event-driven
模型的。正是这个主要的区别带给了 Nginx
在性能上的优势。
Nginx
架构的最顶层是一个 master process
,这个 master process
用于产生其他的 worker process
,这一点和Apache
非常像,但是 Nginx
的 worker process
可以同时处理大量的HTTP
请求,而每个 Apache process
只能处理一个