前端那些事

vuePress-theme-reco chenpeng    2020 - 2021
前端那些事 前端那些事

Choose mode

  • dark
  • auto
  • light
首页
文章目录
  • Browser
  • CSS
  • ES6
  • JavaScript
  • Network
  • TypeScript
  • Vue
  • Vue3
  • Webpack
标签
时间轴
GitHub
author-avatar

chenpeng

85

Article

25

Tag

首页
文章目录
  • Browser
  • CSS
  • ES6
  • JavaScript
  • Network
  • TypeScript
  • Vue
  • Vue3
  • Webpack
标签
时间轴
GitHub
  • Browser

    • 浏览器从输入URL到页面渲染的整个流程
    • 浏览器的重绘与回流
    • 浏览器跨域
    • 浏览器缓存
    • 前端安全
    • localStorage、sessionStorage、cookie、session区别

浏览器跨域

vuePress-theme-reco chenpeng    2020 - 2021

浏览器跨域

chenpeng 2020-11-30 跨域

# 1.跨域

跨域是由于浏览器的同源策略限制,当一个请求 URL 的协议、域名、端口 中任意一个与当前 URL 不同即为跨域

# 2.同源策略

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能都会受到影响,会很容易受到 XSS、CSFR 等攻击。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现

同源策略限制内容:

  • cookie、localStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AIAX 请求不能发送

允许跨域加载资源的标签:

  • <img src="xxx">
  • <link href="xxx">
  • <script src="xxx">
  • <iframe src="xxx">

# 3.跨域实现方式和原理

# 1.JSONP

原理:利用 script 标签没有跨域的限制,网页可以得到从其他来源动态加载的 JSON 数据

优点:简单兼容性好,可用于解决主流浏览器的跨域数据的访问问题

缺点:仅支持 get 方法,不安全可能会受到 XSS 攻击,服务器可能不支持 JSONP

前端传递一个 callback 参数给后端,后端返回一个用 callback 参数包裹住的 json 数据

后端:

const express = require('express')
const app = express()
const port = 3000

app.get('/jsonp', (req, res) => {
    let callback = req.query.callback
    res.send(`${callback}("我是服务端")`)
})

app.listen(port, () => console.log(`Server listening on port ${port}!`))
1
2
3
4
5
6
7
8
9
10

原生 js 实现 jsonp:

let url = 'http://localhost:3000/jsonp?callback=handler'
let script = document.createElement('script')
script.src = url
document.querySelector('head').appendChild(script)

function handler(data){
    console.log(data)// 我是服务端
}

/*利用promise封装*/
function jsonp({ url, params, callback }) {
    return new Promise((resolve, reject) => {
        let script = document.createElement('script')
        window[callback] = function(data) {
            resolve(data)
        }
        params = { ...params, callback }
        console.log(params)
        let arrs = []
        for (let key in params) {
            arrs.push(`${key}=${params[key]}`)
        }
        script.src = `${url}?${arrs.join('&')}`
        console.log(script.src)
        document.body.appendChild(script)
    })
}
jsonp({
    url: 'http://localhost:3000/jsonp',
    params: { id: 1, name: 'cyy' },
    callback: 'handler'
}).then(data => {
    console.log(data)
})
1
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
30
31
32
33
34

JQuery 的 JSONP 形式

$.ajax({
    url:"http://localhost:3000/jsonp?callback=handler",
    dataType:"jsonp",
    type:"get",
    jsonpCallback:"handler",// 自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
    jsonp:"callback",// 传递函数名的形参callback,可省略
    success:function (data){
    	console.log(data);
    }
});
1
2
3
4
5
6
7
8
9
10

# 2.CORS(Cross-Origin Resource Sharing,跨域资源共享)

CORS 需要浏览器和服务端同时支持,浏览器会自动进行 CORS 通信,服务端设置 Access-Control-Allow-Origin 就可以开启 CORS,该属性表示哪些域名可以访问资源

后端:

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', 'http://localhost:1105')
    res.json({name: 'cyy'})
})

app.listen(port, () => console.log(`Server listening on port ${port}!`))
1
2
3
4
5
6
7
8
9
10

前端:

$.ajax({
    url: 'http://localhost:3000/',
    type: 'GET',
    dataType: 'json',
    success: function (data){
        console.log(data)// {name: 'cyy'}
    }
})
1
2
3
4
5
6
7
8

CORS 简单请求和非简单请求

简单请求

只有同时满足以下两种条件,就属于简答请求:

  1. 请求方法是以下三种之一:
    • HEAD
    • GET
    • POST
  2. HTTP 的头信息不超过以下几种字段:
    • Accept(告诉服务端,客户端接收的响应的类型)
    • Accept-Language(客户端支持的语言)
    • Content-Language(请求报文使用的语言)
    • Last-Event-ID
    • Content-Type(请求报文体的类型):只限于三个值
      • application/x-www-form-urlencoded:表单默认的提交数据的格式
      • multipart/form-data:表单中上传文件时的格式
      • text/plain:纯文本格式

简单请求服务器返回的响应头信息:

  1. Access-Control-Allow-Origin:值为请求时 origin 字段的值或者是 * ,表示接受任意域名的请求

  2. Access-Control-Allow-Credentials:表示是否允许发送 cookie ,默认情况下,cookie 不包含在 CORS 请求之中

    • 同时,需要在 Ajax 请求中开启 withCredentials 属性

      var xhr = new XMLHttpRequest();
      xhr.withCredentials = true;
      
      1
      2
  3. Access-Control-Expose-Headers:CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定

非简单请求

非简单请求是对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)

预检请求

  1. 请求的头信息中的特殊字段
    • Access-Control-Request-Method:浏览器 CORS 请求的 HTTP 方法
    • Access-Control-Request-Headers:指定浏览器会额外发送的头信息字段
  2. 响应的头信息
    • Access-Control-Allow-Methods:表示服务器支持的所有跨域请求的方法
    • Access-Control-Allow-Headers:表示服务器支持的所有头信息字段
    • Access-Control-Allow-Credentials
    • Access-Control-Max-Age:指定本次预检请求的有效期

参考

# 3.postMessage

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,postMessage() 方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递

otherWindow.postMessage(message, targetOrigin[, transfer]);
1
  • message: 将要发送到其他 window的数据。
  • targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
  • transfer(可选):是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
// a.html
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> 
//内嵌在http://localhost:3000/a.html
<script>
    function load() {
    let iframe = document.getElementById('iframe')
    iframe.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
    window.onmessage = function(e) { //接受返回数据
        console.log(e.data) //我不爱你
    }
}
</script>
// b.html
<script>
window.onmessage = function(e) {
    console.log(e.data) //我爱你
    e.source.postMessage('我不爱你', e.origin)
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 4.WebSocket

WebSocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器之间的全双工通信,同时也是跨域的一种解决方案,WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

原生WebSocket API使用起来不太方便,可以使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
    socket.send('我爱你');//向服务器发送数据
}
socket.onmessage = function (e) {
    console.log(e.data);//接收服务器返回的数据
}
1
2
3
4
5
6
7

# 5.Node 中间件代理(两次跨域)

实现原理:同源策略是浏览器需要遵循的标准,服务器向服务器请求不需要遵循同源策略

代理服务器,处理步骤:

  1. 接受客户端请求
  2. 将请求转发给服务器
  3. 拿到服务器响应数据
  4. 将数据转发给客户端

# 6.Nginx 反向代理

实现原理:类似于 Node 中间件代理,需要搭建一个 Nginx 中转服务器,用于转发请求

使用 nginx 反向代理实现跨域,是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过 nginx 配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

先下载 nginx ,然后将 nginx 目录下的 nginx.conf 修改如下:

// 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;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  
        #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

7.window.name + iframe

8.location.hash + iframe

a 与 b 跨域通信,需要通过中间页 c 来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。a 传给 b,b 传给 c,c 再传给 a

具体实现:

在a中放一个回调函数,方便c回调。放一个iframe标签,随后传值

<iframe id="iframe" src="http://www.domain2.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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在b中监听哈希值改变,一旦改变,把a要接收的值传给c

<iframe id="iframe" src="http://www.domain1.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>
1
2
3
4
5
6
7
8
9

在c中监听哈希值改变,一旦改变,调用a中的回调函数

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>
1
2
3
4
5
6
7

9.document.domain

跨一级域名相同的域,例如 www.baidu.com 和 www.map.baidu.com 都有 baidu.com,它们之间的跨域访问可以通过设置相同的 document.domain 来进行