如何解决跨域问题

记得要微笑

1、jsonp

jsonp解决跨域问题的本质:<script>标签可以请求不同域名下的资源,即<script>请求不受浏览器同源策略影响。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>jsonp</title>
</head>
<body>
  
</body>
</html>
<script>
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = 'http://127.0.0.1:8888/?callback=foo';
  document.body.appendChild(script);

  function foo(data) {
    console.log(`methods: ${data.methods}, result: ${data.result}`);
    // 请求完后删除添加到页面上的script标签
    var body = document.body
    body.removeChild(script)
  };
</script>

服务端:

const http = require('http')
const urlTool = require('url')

var server = http.createServer((req, res) => {
  var params = urlTool.parse(req.url, true)
  var data = {'methods': 'jsonp', 'result': 'success'}
  res.writeHead(200, {
    'Content-Type': 'text/plain'
  })
  if (params.query && params.query.callback) {
    // 服务器端应当在JSON数据前加上回调函数名,以便完成一个有效的JSONP请求
    var str = `${params.query.callback}(${JSON.stringify(data)})`
  }
  res.end(str);
})
server.listen(8888)
console.log('Server running at http://127.0.0.1:8888/')

2、CORS

跨域资源共享(CORS) 是一种机制,它使用额外的HTTP头来告诉浏览器  让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求

在进行非简单请求的时候,浏览器会先发送一次OPTION请求来“预检”(preflight)该请求是否被允许,请求头中会通过Access-Control-Request-MethodAccess-Control-Request-Headers来告诉服务器我需要用到的方法和字段,服务器通过返回的头部信息中的Access-Control-Allow-OriginAccess-Control-Allow-Method来告诉浏览器该跨域请求是否被允许。当确认允许跨域之后,以后再发送该请求,就会省去预检处理,之间当作简单请求来操作。

简单请求与非简单请求的区别

* 请求方式:HEAD,GET,POST
* 请求头信息:
    Accept
    Accept-Language
    Content-Language
    Last-Event-ID
    Content-Type 对应的值是以下三个中的任意一个
        application/x-www-form-urlencoded
        multipart/form-data
        text/plain

只有同时满足以上两个条件时,才是简单请求,否则为非简单请求

3、Nginx代理

3.1、正向代理和反向代理的概念

无论是正向代理,还是反向代理,说到底,就是代理模式的衍生版本罢了。我们都学习过代理设计模式,都知道代理模式中有代理角色和被代理角色,为什么这么说,因为这两个角色对于我们理解正向和反向代理非常重要,下面会讲到。

下面我将引入这样一个场景,很多时候我们上网的网速特别慢,或者说由于fanqiang问题导致我们无法访问到国外的网站,通常这些情况我们会通过给浏览器配置一个网速快的、可以fanqiang的代理ip及端口号来解决我们的问题,那么配置好之后,大概的请求流程如下图所示:

我们首先请求代理服务器,然后代理服务器帮我们去快速访问国外的网站,对于这种代理方式,我们就称之为正向代理。请记住,上面说到代理模式的两个角色中,我们当前的角色为 被代理者,也就是浏览器这个角色。更重要的是,正向代理的本质是我们去请求外部的资源,如果以生产者、消费者模式来区分,我们属于消费者。

总结:

  • 1、正向代理,我们的角色是 被代理者
  • 2、正向代理,我们不对外提供服务,反而是对外消费服务,属于消费者

反向代理,很显然,就是和正向代理相反,如果说正向代理是男,那么反向代理就是女了,亲,此处不再纠结其他情况!下面我用一副图片解释下反向代理:

看完上面的图片,请你想象一下这么一个场景,假设你现在是某公司技术总监,你们公司需要对外提供一套web服务,那么你打算怎么做呢?

答案是可以通过反向代理来完成。通常你们公司拥有自己的IDC机房,机房通讯通常采用局域网交换机,internet网用户请求是无法直接访问到局域网内的web服务的,因此这个时候,你需要一台反向代理服务器来接收internet web请求,然后将请求分发到局域网中的不同主机上进行处理,处理完成之后再做出响应。因此,反向代理大概就是这么一个场景。请记住,反向代理中,我们的角色是 局域网 web服务

总结:

  • 1、反向代理,我们的角色是 局域网 web服务
  • 2、反向代理,我们对外提供服务,属于服务提供者

3.2、nginx反向代理实现跨域

3.2.1、前端配置nginx方向代理实现跨域

场景:假如前端页面urlhttp://127.0.0.1:5500需要向后端http://127.0.0.1:3000发起请求(http://127.0.0.1:3000/index),由于浏览器的同源策略,该请求跨域了。前端可以配置nginx方向代理,将前端服务http://127.0.0.1:5500代理到http://127.0.0.1:3003,前端请求由http://127.0.0.1:3000/xxx改成http://127.0.0.1:3003/xxx,后端服务http://127.0.0.1:3000代理到http://127.0.0.1:3003,这样前后端交互就同源了。核心思想就是将前后端服务代理到同一域下

// http://127.0.0.1:5500页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>
    <button id="getOK">发送请求OK(客户端解决跨域问题)</button>
    <button id="getNO">发送请求NO(客户端解决跨域问题)</button>
    <script>
        $(document).ready(function () {
            $('#getOK').click(function () {
                $.ajax({
                    url:'http://127.0.0.1:3003/ok',
                    success:function(res) {
                        console.log("success",res)
                    },
                    error:function(err) {
                        console.log('fail',err)
                    }
                })
            })
            $('#getNO').click(function () {
                $.ajax({
                    url:'http://127.0.0.1:3003/no',
                    success:function(res) {
                        console.log("success",res)
                    },
                    error:function(err) {
                        console.log('fail',err)
                    }
                })
            })
        })
        

    </script>
</body>
</html>
// http://127.0.0.1:3000后端
const express = require('express')
const cookieParser = require('cookie-parser')

var app = express()


var router = express.Router()
router.get('/ok',function (req,res) {
    res.json({
        code:200,
        msg:"isOK"
    })    
})

router.get('/ok/son',function (req,res) {
    res.json({
        code:200,
        msg:"isOKSon"
    })    
})

router.get('/ok2',function (req,res) {
    res.json({
        code:200,
        msg:"isOK2"
    })    
})

router.get('/no',function (req,res) {
    res.json({
        code:200,
        msg:"isNO"
    })    
})

router.get('/no/son',function (req,res) {
    res.json({
        code:200,
        msg:"isNOSON"
    })    
})

router.get('/no/son2',function (req,res) {
    res.json({
        code:200,
        msg:"isNOSON2"
    })    
})

app.use(router)
app.use(cookieParser)
app.listen(3000,function () {
    console.log('listen in 3000')
})
// nginx反向代理
server {
    listen       3003;
    server_name  127.0.0.1; // 不能加上http:// ,否则会报server name "\*" has suspicious symbols

    location / {
       proxy_pass http://127.0.0.1:5500;
    } 
    location /no {
       proxy_pass http://127.0.0.1:3000;
    }
    location /ok/ {
       proxy_pass http://127.0.0.1:3000;
    }
}

效果:
image.png

3.2.2、后端配置nginx方向代理实现跨域

思想:跟前端配置nginx方向代理有所不同,后端nginx方向代理实现跨域只需要代理后端服务,并在代理服务器上配置响应头属性允许前端访问代理服务器。

// http://127.0.0.1:5500 前端页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>
    <button id="get">发送请求(服务端解决跨域问题)</button>
    <script>
        $(document).ready(function () {
            $('#get').click(function () {
                $.ajax({
                    url:'http://127.0.0.1:3002/ok',
                    //  带cookies的请求
                    xhrFields:{
                        withCredentials:true
                    },
                    success:function(res) {
                        console.log("success",res)
                    },
                    error:function(err) {
                        console.log('fail',err)
                    }
                })
            })
        })
        

    </script>
</body>
</html>
// http://127.0.0.1:3000 后端
const express = require('express')
const cookieParser = require('cookie-parser')

var app = express()


var router = express.Router()
router.get('/ok',function (req,res) {
    res.json({
        code:200,
        msg:"isOK"
    })    
})

router.get('/ok/son',function (req,res) {
    res.json({
        code:200,
        msg:"isOKSon"
    })    
})

router.get('/ok2',function (req,res) {
    res.json({
        code:200,
        msg:"isOK2"
    })    
})

router.get('/no',function (req,res) {
    res.json({
        code:200,
        msg:"isNO"
    })    
})

router.get('/no/son',function (req,res) {
    res.json({
        code:200,
        msg:"isNOSON"
    })    
})

router.get('/no/son2',function (req,res) {
    res.json({
        code:200,
        msg:"isNOSON2"
    })    
})

app.use(router)
app.use(cookieParser)
app.listen(3000,function () {
    console.log('listen in 3000')
})
// 后端nginx方向代理
server {
    listen 3002;
    server_name 127.0.0.1;
    location /ok {
        proxy_pass http://127.0.0.1:3000;

        #   指定允许跨域的方法,*代表所有
        add_header Access-Control-Allow-Methods *;

        #   预检命令的缓存,如果不缓存每次会发送两次请求
        add_header Access-Control-Max-Age 3600;
        #   带cookie请求需要加上这个字段,并设置为true
        add_header Access-Control-Allow-Credentials true;

        #   表示允许这个域跨域调用(客户端发送请求的域名和端口) 
        #   $http_origin动态获取请求客户端请求的域   不用*的原因是带cookie的请求不支持*号
        add_header Access-Control-Allow-Origin $http_origin;

        #   表示请求头的字段 动态获取
        add_header Access-Control-Allow-Headers 
        $http_access_control_request_headers;

        #   OPTIONS预检命令,预检命令通过时才发送请求
        #   检查请求的类型是不是预检命令
        if ($request_method = OPTIONS){
            return 200;
        }
    }
}

实际上不是非简单请求的且不带cookie只需2个字段即可解决跨域
add\_header Access-Control-Allow-Methods \*;
add\_header Access-Control-Allow-Origin $http\_origin;
效果:
image.png

window下常用nginx命令:
查看nginx进程
tasklist /fi "imagename eq nginx.exe"
杀掉nginx进程
taskkill /f /pid 16900 /pid 19012
启动nginx
start nginx

4、postMessage

场景1:在a页面里打开了另一个不同源的页面b,你想要让a和b两个页面互相通信。比如,a要访问b的LocalStorage。

场景2:你的a页面里的iframe的src是不同源的b页面,你想要让a和b两个页面互相通信。比如,a要访问b的LocalStorage。

解决方案:HTML5y引入了一个全新的API,跨文档通信 API(Cross-document messaging)。这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。a就可以把它的LocalStorage,发送给b,b也可以把自己的LocalStorage发给a。

window.postMessage(message, targetOrigin, [transfer]),有三个参数:

message是向目标窗口发送的数据;
targetOrigin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI(或者说是发送消息的目标域名);
transfer可选参数,是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

另外消息的接收方必须有监听事件,否则发送消息时就会报错。The target origin provided ('http://localhost:8080') does not match the recipient window's origin ('http://localhost:63343').

window.addEventListener("message",onmessage);onmessage接收到的message事件包含三个属性:

data:从其他 window 中传递过来的数据。
origin:调用 postMessage 时消息发送方窗口的 origin 。请注意,这个origin不能保证是该窗口的当前或未来origin,因为postMessage被调用后可能被导航到不同的位置。
source:对发送消息的窗口对象的引用; 您可以使用此来在具有不同origin的两个窗口之间建立双向通信。

案例
使用http-server模拟不同源的两个页面

// http://127.0.0.1:8080/A.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>
  <button onclick="send()">向弹窗页发送信息</button>
</body>

</html>

<script>
  var popup;
  window.onload = function () {
    popup = window.open('http://127.0.0.1:8081/C.html', '', 'width=500,height=500')
  }

  function send() {
    popup.postMessage('Hello World!', 'http://127.0.0.1:8081/')
  }
</script>
// http://127.0.0.1:8081/C.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>
    <div id="app"></div>
</body>

</html>

<script>
    window.onload = function () {
        console.log('recieve')
        window.addEventListener("message", onmessage);
    }
    function onmessage(event) {
        if (event.origin == "http://127.0.0.1:8080") { // http://localhost:8080是发送方a的域名
            document.getElementById('app').innerText = event.data
        }
    }
</script>

5、document.domain

6、window.name

场景1:现在浏览器的一个标签页里打开http://www.damonare.cn/a.html页面,你通过location.href=http://baidu.com/b.html,在同一个浏览器标签页里打开了不同域名下的页面。这时候这两个页面你可以使用window.name来传递参数。因为window.name指的是浏览器窗口的名字,只要浏览器窗口相同,那么无论在哪个网页里访问值都是一样的。

场景2:你的http://www.damonare.cn/a.html页面里使用<iframe>调用另一个http://baidu.com/b.html页面。这时候你想在a页面里获取b页面里的dom,然后进行操作。然后你会发现你不能获得b的dom。同样会因为不同源而报错,和上面提到的不同之处就是两个页面的一级域名也不相同。这时候document.domain就解决不了了。

解决方案:浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。。比如你在b页面里设定window.name="hello",你再返回到a页面,在a页面里访问window.name,可以得到hello

这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。

// http://127.0.0.1:8080
<!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>
  <p></p>
  <iframe src="http://127.0.0.1:8081/demo10.html" frameborder="0"></iframe>
</body>
</html>
<script>
  var ifr = document.getElementsByTagName('iframe')[0]
  ifr.onload = function() {
    document.getElementsByTagName('p')[0].innerText = ifr.contentWindow.name
  }
</script>
// http://127.0.0.1:8081
<!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="hello, world!"
  </script>
</body>
</html>

上面执行结果会报错,因为不同源
image.png
image.png
解决方法:

<script>
  var ifr = document.getElementsByTagName('iframe')[0]
  ifr.onload = function() {
    ifr.onload = function(){
      document.getElementsByTagName('p')[0].innerText = ifr.contentWindow.name
    }
    ifr.src = 'about:blank';
  }
</script>

参考:
https://segmentfault.com/a/11...
https://segmentfault.com/a/11...
https://juejin.im/entry/5b3a2...
https://juejin.im/post/5b67b3...
https://juejin.im/post/5815f4...

阅读 2k
avatar
记得要微笑
前端工程师

求上而得中,求中而得下,求下而不得

1.5k 声望
4.1k 粉丝
0 条评论
avatar
记得要微笑
前端工程师

求上而得中,求中而得下,求下而不得

1.5k 声望
4.1k 粉丝
文章目录
宣传栏