跨域

什么是跨域?
违反了浏览器同源策略的都是跨域

同源策略

何谓之同源?
同源即同协议、同域名、同端口,否则为跨域
同源策略会阻止一个域的JavaScript脚本和另外一个域的内容进行交互

跨域的表现

跨域例:
http://www.test.cn:3000https://www.test.cn:3000

不同源时,以下操作会受影响的:

  • js操作本地存储如cookie、LocalStorage
  • js操作页面DOM元素
  • 可以发送ajax请求并且被服务端正常响应,但响应结果会被浏览器拦截(Cross-Origin Read Blocking)

为什么浏览器不支持跨域

  • 安全方面,防止窃取cookie。用户可能给恶意网站发请求,恶意网站拿到用户cookie
  • DOM方面,如果可以操作DOM,可能嵌入iframe,造成安全问题

跨域的9种解决方案

  • jsonp
  • cors (cross origin resource sharing) 跨域资源共享
  • postMessage
  • document.domain(主域和副域名)
  • window.name
  • location.hash
  • nginx
  • websokcet(页面之间通信)
  • http-proxy

1 jsonp

jsonp, 即JSON padding,跨域获取json的方式
浏览器拦截跨域请求,只针对js
因此,一些具有引入外部资源属性的标签,在引入时并不会被浏览器的同源机制拦截。在此基础上可以有如下方案:

  • 通过link标签的href
  • 通过img标签的src
  • 通过script标签的src

下面给出基于script的实现方案

  1. 创建script标签,src属性指向外部资源
  2. 在src上指定回调函数,并将json数据携带在回调函数的参数上响应给客户端
  3. 客户端接收响应,将src请求回来的脚本字符串放在script标签体内执行,调用设置好的回调函数,获取数据
方式一: 本地开启服务,jsonp获取数据

html:

<! DOCTYPE html>
<html lang="en">

<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>

<body>

    <script>
        
        //创建script标签,并引用外部资源
        function jsonp(params) {
            return new Promise(function (resolve, reject) {
                const { url, query, cb } = params
                const address = `${url}?wd=${query.wd}&cb=${cb}`
                let script = document.createElement('script')
                script.src = address
                // 执行回调
                window[cb] = function (data) {
                    resolve(data)
                    // 垃圾回收
                    document.body.removeChild(script)
                }

                document.body.appendChild(script)
            })
        }

        const getJsonpRes = async function () {
            let rs = await jsonp({
                url: 'http://localhost:3000/say',
                // 查询字段
                query: { wd: 'island' },
                // 回调函数
                cb: 'getCRData'
            })
            console.log('result from server:',rs)
            return rs
        }

        getJsonpRes();

    </script>

</body>

</html>

server:

let express = require('express')
let app = express()
app.get('/say', function(req, res){

    let {wd,cb} = req.query
    console.log(wd,cb)
    //将数据作为回调函数的参数返回
    const jsonData = JSON.stringify([{name:"island"}]) 
    res.end( `${cb}(${jsonData})` )

})
app.listen(3000, () =>console.log('server run at 3000'))
方式二:使用百度搜索的服务
<! DOCTYPE html>
<html lang="en">

<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>

<body>

    <script>
        //创建script标签,并引用外部资源
        function jsonp(params) {
            return new Promise(function (resolve, reject) {
                const { url, query, cb } = params
                const address = `${url}?wd=${query.wd}&cb=${cb}`
                let script = document.createElement('script')
                script.src = address

                // 执行回调
                window[cb] = function (data) {
                    //返回数据
                    resolve(data)
                    // 垃圾回收
                    document.body.removeChild(script)
                }

                document.body.appendChild(script)
            })
        }

        const getJsonpRes = async function () {
            let rs = await jsonp({
                url: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',
                // 查询字段
                query: { wd: 'island' },
                // 回调函数
                cb: 'getCRData'
            })
            console.log(rs)
            return rs
        }

        const getJsonpRes2 = function () {
            jsonp({
                url: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',
                query: { wd: 'island' },
                cb: 'getCRData'
            }).then(rs => {
                console.log(rs)
            })
        }

        // getJsonpRes();
        // getJsonpRes2();

    </script>

</body>

</html>
缺陷:
  • 只支持get请求,不支持post、put、delete。
  • 不安全,可能有xss攻击(可能返回一个script标签)

2 cors

cors,即cross origin resource sharing,跨域资源共享
这种方式主要是在后端实现,通过设置各种响应头,来保证响应数据不被浏览器拦截

客户端

<! DOCTYPE html>
<html lang="en">

<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>

<body>

    hello world 3000

    <script>
        let xhr = new XMLHttpRequest()
        // 第三个参数指定异步或者同步
        // xhr.open('GET', 'http://localhost:4000/getData',true)

        // (4) 强制带上凭证
        document.cookie = 'name=island'
        xhr.withCredentials = true
        
        //(2) 可选设置请求头名字,如果设置了,服务端也要设置响应的Access-Control-Allow-Headers
        // xhr.setRequestHeader('name','island')

        // (3)
        xhr.open('PUT', 'http://localhost:4000/getData', true)
        
        xhr.onreadystatechange = function () {
            //304 缓存
            if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 300 || xhr.status === 304) {
                console.log(xhr.response)
                // (5) 获取返回头
                console.log(xhr.getResponseHeader('name'))
            }
        }
        xhr.send()

    </script>

</body>

</html>

本地服务

const express = require('express')

const app = express()

//启用静态资源
app.use(express.static(__dirname))

app.listen(3000, ()=>console.log('server run at 3000'))

外部服务

const express = require('express')
const app = express()
//设置访问白名单
let accessList = ['http://localhost:3000']
app.use(function (req, res, next) {

    let origin = req.headers.origin
    if (accessList.includes(origin)) {
        //设置允许跨域访问服务的白名单,当设置为*的时候,设置携带cookie失效
        res.setHeader('Access-Control-Allow-Origin', origin)
        // 设置允许的请求头名字
        res.setHeader('Access-Control-Allow-Headers', 'name')
        //get post是默认支持的,不用配,其他的需要配置
        res.setHeader('Access-Control-Allow-Methods', 'PUT')
        // 预检options的存活时间,即都少秒之后重新预检
        // res.setHeader('Access-Control-Max-Age',6)
        //允许携带cookie
        res.setHeader('Access-Control-Allow-Credentials', true)
        //允许设置响应头
        res.setHeader('Access-Control-Expose-Headers','name')
        if (req.method === 'OPTIONS') {
            //OPTIONS请求不做任何处理,因为是试探预检请求,确定接口正常才可以发送请求体
            // res.end()
            console.log('接收到了OPTIONS请求')
        }
    }
    next()

})
app.get('/getData', function (req, res) {

    console.log('接收到了请求')
    res.end('data from 4000')

})
app.put('/getData', function (req, res) {

    console.log('接收到PUT了请求')
    // 设置响应头
    res.setHeader('name', 'island header name')
    res.end('data from 4000')

})
app.listen(4000, () => console.log('server run at 4000'))

3 postMessage

主要api:
iframe.contentWindow
window.postMessage
onmessage, e.soure

实现:

  1. 通过iframe嵌套需要发送信息的网页,并监听onload事件保证网页加载完毕
  2. 在load回调里通过iframe.contentWindow获取对方网页,使用iframe.contentWindow.
  3. postMessage向对方网页发送信息,对方网页通过window监听onmessage获取信息
  4. 对方网页在onmessage回调中,e.source.postMessage向我放网页发送信息
  5. 我方window监听onmessage,获取响应信息。实现了双向通信

本地网页

<! DOCTYPE html>
<html lang="en">
<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>

    <!-- 借用iframe+onload事件+postMessage -->
    <iframe src="http://localhost:4000/b.html" id="frame" onload="load()" frameborder="0"></iframe>
    <script>
        function load(params){
            //和b窗口通信
            let frame = document.getElementById('frame')
            // 给b窗口发送信息,postMessage(data,url)
            frame.contentWindow.postMessage('a send msg to b','http://localhost:4000')

            //监听b的回复
            window.onmessage = (e)=>{
                console.log(e.data)
            }
        }

    </script>

</body>
</html>

本地服务

const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(3000, ()=>console.log('server run at 3000'))

对方网页

<! DOCTYPE html>
<html lang="en">
<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>

    this is b.html
    <script>
        window.onmessage = (e)=>{
            console.log(e.data, e)
            //回复给a窗口
            e.source.postMessage('b send msg to a', e.origin)
        }

    </script>

</body>
</html>

对方服务

const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(4000, ()=>console.log('server run at 4000'))

4 window.name

思路:
iframe+onload

虽然iframe可以通过src引入外部网页,但是不能拿到外部网页的DOM或者属性,因为这是跨域的
因此可以在第一次onload时,改变src,切换到同源网页,此时name属性依然还在,完成跨域获取数据

iframe引用外部网页,在外部网页中设置window.name属性
iframe监听onload事件,第一次触发onload事件时,改变iframe的src指向同源的window,并toggle bool
iframe第二次触发onload事件,拿到iframe.contentWindow.name

本地页面

<! DOCTYPE html>
<html lang="en">

<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>

<body>

    a和b是同域的 3000
    c是独立的 4000
    a获取c的数据

    a先引用c c把值放到window.name,把a页面的iframe的引用地址改到b,name不会消失(标签没变,只是标签的属性变化,所以src变化window.name依然还在)

    <iframe id="frame" src="http://localhost:4000/c.html" onload="load()" frameborder="0"></iframe>
    <script>
        let first = true
        function load() {
            let frame = document.getElementById('frame')
            //虽然可以通过src引入外部网页,但是不能拿到外部网页的DOM或者属性,因为这是跨域的
            // console.log(frame.contentWindow.name)
            
            // 初次进入,加载b页面
            if (first) {
                frame.src = 'http://localhost:3000/b.html'
                first = false
                return
            } else {
                console.log(frame.contentWindow.name)
            }
        }

    </script>

</body>

</html>

本地页面b,作为中间桥梁

<! DOCTYPE html>
<html lang="en">
<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>

    this is b

</body>
</html>

本地服务,提供静态资源访问

const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(3000, ()=>console.log('server run at 3000'))

外部网页

<! DOCTYPE html>
<html lang="en">
<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>
    <script>
        window.name = '这是4000下, c中的window name'

    </script>

</body>
</html>

外部服务,提供静态资源访问

const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(4000, ()=>console.log('server run at 4000'))

5 hash

思路:

  1. a通过iframe引用c,并在url后面设置hash值hasha,监听hashchange事件
  2. c用loaction.hash收到hasha之后,通过iframe引用和a同源的b,并在iframe的src上设置hash值hashc
  3. b通过location.hash拿到hashc,并通过window.parent.parent拿到窗口a,给a设置hashc,设置完之后,a的hashchange事件触发,拿到c传过来的hash值

本地服务和外部服务

const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(3000, ()=>console.log('server run at 3000'))
const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(4000, ()=>console.log('server run at 4000'))

本地网页

<! DOCTYPE html>
<html lang="en">
<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>
    page a
    <!-- 
        路径后面的hash值可以用来通信
        a和b同
        c独立域
     -->
    <!-- 目的a想访问c -->
    <!-- a给c传一个hash值,c收到hash值后把hash值传给b -->
    <!-- c给hash值传给b, b将结果放到a的结果中 -->
    <iframe src="http://localhost:4000/c.html#req_island" frameborder="0"></iframe>
    <script>
        window.onhashchange = function(){
            console.log(location.hash)
        }
    </script>
</body>
</html>

和本地网页同源的网页

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    this is b
    <script>
        window.parent.parent.location.hash = location.hash
    </script>
</body>
</html>

外部网页

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- 
        c拿到之后创建iframe指向b
        b和a同域
     -->
    <script>
        console.log('a传给c的hash是:',location.hash)
        let iframe = document.createElement('iframe')
        iframe.src = 'http://localhost:3000/b.html#res_island'
        document.body.appendChild(iframe)
    </script>
</body>
</html>

6 domain

domain主要用于一级域名和二级域名之间的跨域通信
只要一级域名和二级域名之间设置document.domain的值相同
就可以通过iframe.contentWindow拿到对方的window的属性实现通信

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- 
        一级域名二级域名
        思路:
            a通过http://a.island.cn:3000/b.html:3000/a.html
            b是通过http://b.island.cn:3000/b.html:3000/b.html
            通过iframe,再用document.domain声明是一家的域名,就可以访问了
            局限是主域名和副域名之间可以用
     -->
    hello a 
    <iframe id="frame" src="http://b.island.cn:3000/b.html" frameborder="0" onload="load()"></iframe>
     <script>
         //2 domain,告诉是一家的
         document.domain = 'island.cn'

         function load(){
            let frame = document.getElementById('frame')
            // 拿到b窗口的window下属性,跨域
            frame.contentWindow.propsb
         }
     </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    hello b
    <script>
        document.domain = 'island.cn'
        window[propsb] = 'this is propsb from b'
    </script>
</body>

</html>

7 websocket

不同于基于JavaScript的ajax,websocket没有跨域限制
websocket协议是ws,内部基于tcp
socket和http的本质区别在于,socket是双向的,http是单向的
作为h5的高级API,websocket的一个缺陷是兼容性不太好,但是有一个常用的库兼容性很强,即socket.io

建立websocket通信
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
   
     <script>
         //新建websocket通信
         let socket = new WebSocket('ws://localhost:3000')
         //给websocket服务器发送信息
         socket.onopen = function(){
             socket.send('island send')
         }
         //监听socket服务器返回的信息
         socket.onmessage = function(e){
             console.log(e.data)
         }
     </script>
</body>
</html>
建立websocket服务

yarn add ws

let Websocket = require('ws')

let wss = new Websocket.Server({port:3000})

wss.on('connection',function(ws){
    ws.on('message',function(data){
        console.log(data)

        ws.send('island server response')
    })
})

8 Nginx 配置跨域

nginx跨域是最简单的跨域
配置nginx.conf
//所有json后缀的,在nginx文件夹的json目录下查找
location ~..json{
# 指定根目录下 json目录
root json;
# 添加跨域头,此来源将被列入白名单
add_header "Access-Control-Allow-Origin" "
"
}

9 webpack配置跨域

webpack可以在devServer中配置跨域,常见配置如下

target:表示目标资源的地址
pathRewrite:对该字段重写
changeOrigin:核心配置。为true时会将请求头中的host字段改成target
secure:设置https协议的代理,因为target默认不支持https协议,因此需要做额外的配置

10 拓展:简单请求复杂请求跨域 跨域的cookie携带问题

拓展:简单请求复杂请求跨域 跨域的cookie携带问题