一,源
概念:源(origin)是由协议、IP地址/域名、端口号组成。
二,同源
概念:两个URL的协议、IP地址/主域名、端口号均一致,即为同源。(子域名可以不同:例如http:www.baidu.com/search和http:www.baidu.com/jump)
同源策略:浏览器为了防止XSS、CSRF等攻击,而设计了同源策略,来保障网络安全。
三,非同源
概念:同源的任意一个条件不满足,即为非同源。
非同源的限制:
- 可以访问其他源的图片、js、css(包括<img src="其他源地址">,<link href="其他源地址">,<script src="其他源地址">)
- form表单可以提交,但是不需要返回
- ajax请求发送之后,浏览器会拦截响应(因此跨域情况下服务端是可以收到ajax的请求)
- 不可以访问其他源的cookie、localStorge
- 不能操作dom
- 不能访问iframe嵌入的其他页面内容
四,跨域
所谓跨域CROS,即cross origin resource sharing,跨源资源共享。用来解决浏览器的同源限制,访问其他源的资源。
五,跨域的解决方式
5.1 jsonp方式
原理:他是一个传统的跨域解决方式,主要是利用script标签可以引用其他源的地址。
1,客户端动态创建script标签,通过自定义一个回调函数拼接到src属性的URL中,来实现向接口发送请求
2,服务器接收到请求后,根据回调函数动态生成客户端想要的脚本,并以字符串的形式返回
3,客户端将服务器返回的字符串当成js解析,并执行
实现示例:
<!-- 浏览器 -->
<!-- 通过src属性请求app/datalist接口,传参callback=getData -->
<script src="http://kuayu.com/app/datalist?callback=getData">
<script>
// 定义回调函数 和参数
function getData(data) {
console.log(data); // init success
}
</script>
// 服务器
// 定义接口app/datalist
app.get('/app/datalist', (req, res) => {
let fn = req.param.callback; // 获取callback参数的值
const resData = 'init success';
res.send(fn + resData); // 服务端响应回调函数和参数
})
特点:
优点:
- 兼容性较好(可兼容老版本的IE浏览器)
缺点:
- 不能控制请求过程。因为他只是一个script标签,由浏览器自动发起的请求
- 只支持get请求方式。
5.2 CORS方式
原理:
就是在请求头添加一个身份来源标识,同时服务器端根据标识判断是否可以访问,如果允许则添加一个标记并返回响应。CORS同样分为简单请求和非简单请求两种。
简单请求:请求方式是get、post或head方式;请求头只能设置基础的安全字段(accept、accept-lauage、content-language、content-type这几个字段)
非简单请求:除简单请求之外的其他请求。例如put、delete
实现示例:
简单请求:http请求头只需设置origin属性为跨域的源即可
GET /api HTTP/1.1
Origin: http://www.xxx.com //本次请求来自哪个源
Host: http://www.xx.com //请求的第三方API
Accept-Language: en-US
服务器设置Access-Control-Allow-Origin的支持访问的源。服务器收到响应会和origin的源进行对比,如果在支持的源内,则返回响应,同时在响应的header中添加设置的属性
Access-Control-Allow-Origin: http://www.xxx.com //请求允许的源
Access-Control-Allow-Credentials: true //是否允许cookie
Content-Type: text/html; charset=utf-8
复杂请求:
客户端会先发一次options预请求,服务器检查Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段,确认允许访问之后,再做出响应,然后客户端再发正式的请求。
options预请求和响应都没有请求体body
特点:
- w3c增加的新特性,需要服务端支持,同时不兼容IE9以下浏览器
- 相比jsonp,支持的方法更多,更灵活
- 可以根据请求方式的不同灵活选用简单请求和复杂请求方式
5.3 websocket方式
原理:
websocket可以实现浏览器和服务器的双向通信,因此不会有跨域的问题。
特点:
- 因为他是常链接,所以需要根据业务场景来判断是否选用
- 原生的websocketAPI使用起来较麻烦,需要引入三方库例如socket.io来实现
5.4 服务端代理
原理:因跨域是浏览器和服务器之间的限制,而服务器和服务器之间没有这种限制,因此增加一个代理服务器,来解决跨域问题。
主要流程是:接受客户端的请求=>将请求转发给服务器=>接受服务器的请求=>将响应转发给客户端
特点:
- 客户端和服务器不需要添加额外的判断逻辑和代码
- 需要额外实现一个服务器端的代码开发(例如node中间件)
5.5 nginx反向代理
原理:
与服务器代理原理类似,只是这个需要搭建一个nginx服务器来转发请求。
特点:
- 只需要修改nginx.conf文件中的配置即可,不需修改任何代码,实现简单
- 支持所有浏览器
- 不会影响服务器的性能
- 支持session
实现示例:
server{
listen 8080;
server_name http://www.xx.com;
location / {
proxy_pass http://www.yy.com:8000; # 反向代理
}
}
5.6 webpack的proxy代理配置
原理:
通过配置webpack的配置文件模拟本地服务的接口,而非代理的方式,相当于直接请求本地的服务devServer。
特点:
- 只需要修改webpack的配置文件即可
- 只支持本地跨域解决方案,线上环境无效
实现示例:
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://xxx.com',
pathRewrite: {'^/api' : ''},
changeOrigin: true, // target如果是域名时,需要这个参数
secure: false, // 设置支持https协议的代理
}
}
}
}
5.7 window.postMessage
原理:
window.postMessage是H5新增的特性,主要为了解决跨窗口之间的通信,也支持iframe下进行通信。可以利用这个方法来绕过浏览器的跨域。
特点:
- 不需要服务端处理,客户端通过这个方法绕过了同源策略
- 是H5新增的window的原生属性
- 可支持多窗口通信
使用示例:
window.postMessage(message, targetOrigin)方法主要入参分别为:
- message:发送的消息内容
- targetOrigin:要发送的目标网址(设置*号为向所有窗口发送消息)
window.onmessage函数接受的event参数提供了3个属性值:
- event.source:发送消息的窗口
- event.origin: 消息发向的网址
- event.data: 消息内容
// 标签一,发送
window.postMessage({ name: '张三', age: 18 }, 'http://127.0.0.1:8080');
window.onmessage = function(e) {
console.log(e.data); // 接收另一个标签的响应
}
// 标签二,接收、响应
window.onmessage = function(e) {
console.log(e.data);
e.source.postMessage('收到', e.origin); // 向接收来的标签响应一个消息
}
5.8 document.domain + iframe
原理:
两个相同的二级域名(例如a.test.com和b.test.com)之间,可以同时设置document.domain = "test.com"为基础域名,强制设置为同域,来实现通信。因此可以利用iframe嵌套网页,然后在他的onload事件中,调用contentWindow属性来获取另一个页面的值。
实现示例:
<!-- 页面1 -->
<iframe src="http://b.test.com" onload="load()" id="frame"></iframe>
<script>
documnet.domain = "test.com"; // 定义二级域名
load() {
console.log(frame.connectWindow.text); // 获取另一个页面的text属性
}
</script>
<!-- 页面2 -->
<script>
documnet.domain = "test.com"; // 定义二级域名
var text = "好利来"; // 定义变量
</script>
页面1实现方式2:
<!-- 页面1 -->
<iframe src="http://b.test.com" id="frame"></iframe>
<script>
documnet.domain = "test.com"; // 定义二级域名
documnet.getElementById("frame").onload = function() {
console.log(this.connectWindow.text); // 获取另一个页面的text属性
}
</script>
特点:
- 只需客户端修改代码
- 条件限制必须是二级域名相同
5.9 window.name + iframe
原理:
window.name属性有一个特性,就是无论是否同源,只要在一个窗口,就可以访问前一个网页设置的window.name属性值;而且他的值存储最大可以支持到2M。因此可以将变量存到window.name中,然后利用iframe嵌套访问。
实现示例:
<!-- 页面1 -->
<iframe src="http://b.test.com" onload="load()" id="frame"></iframe>
<script>
load() {
// 获取另一个页面的window.name属性
const params = frame.connectWindow.name;
const arrs = params.split(";");
}
</script>
<!-- 页面2 -->
<script>
window.name = "token=xxx;id=xxx"; // 存储一些信息
</script>
特点:
- window.name容量很大,可以存储2M的字符串
- 同样是绕过浏览器的同源策略,不需要修改服务端代码
- 必须要监听窗口的window.name属性变化,影响页面的性能
5.10 location.hash + iframe
原理:
三个页面。需要通过实现一个中间页,利用iframe的location.hash来传值。例如a.html和c.html是同域;b.html是跨域。
a.html和b.html、b.html和c.html不同域通过location.hash传值并访问,且是单向通信;
而c.html和a.html是同域,通过window.parent.parent属性去访问
实现示例:
略
特点:
- 实现起来繁琐,只是说明有这种实现方式而已
- a.html和b.html不同域只能单向传值