34

跨域是我们在项目中经常遇到的,前后端数据交互经常碰到请求跨域,首先我们来想一下为什么会有跨域这个词的出现?本文带你来探讨一下以下几个问题:

  • 跨域是什么?
  • 为什么要跨域?
  • 跨域的几种方式?
  • ...

什么是跨域?

跨域是指的浏览器不能执行其它网站的脚本,它是由浏览器的同源策略造成,是浏览器对JavaScript实施的安全限制。

跨域实际上指从一个域的网页去请求另一个域的资源,比如:从 http://www.baidu.com 网站去请求http://www.google.com 网站的资源。

什么是同源策略?

同源策略 指的是 域名协议端口 三者都相同~

什么是同源?

要知道URL协议域名端口以及路径组成,若两个URL的协议、域名和端口相同,则表示他们同源。
相反,只要协议域名端口有任何一个的不同,就被当作是跨域。

限制同源策略内容

  • Cookie、LocalStorage、IndexedDB等存储性内容
  • DOM节点
  • Ajax请求发送后,结果被浏览器拦截了

允许跨域加载资源

这下边三个含有 src 标签的是允许跨域加载资源的

<img src=XXX>
<link href=XXX>
<script src=XXX>

跨域的场景

九种跨域解决方案

  • jsonp
  • cors
  • postMessage
  • document.domain
  • window.name
  • location.hash
  • https-proxy
  • nginx
  • websocket

jsonp

什么是jsonp

jsonp全称是JSON with Padding,是为了解决跨域请求资源而产生的解决方案,是一种依赖开发人员创造出的一种非官方跨域数据交互协议。

Jsonp的原理

  1. 利用script标签的src属性来实现跨域
  2. 通过将前端方法作为参数传递到服务器端,然后由服务器端注入参数之后再返回,实现服务器端向客户端通信
  3. 由于使用script标签的src属性,因此只支持get方法

Jsonp和Ajax对比

  1. Jsonp和Ajax相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式
  2. Ajax属于同源策略
  3. Jsonp属于非同源策略(跨域请求)

Jsonp的优缺点

优点:

  1. 它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制,JSONP可以跨越同源策略
  2. 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持
  3. 在请求完毕后可以通过调用callback的方式回传结果

缺点:

  1. 它只支持GET请求而不支持POST等其它类型的HTTP请求
  2. 它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题
  3. jsonp在调用失败的时候不会返回各种HTTP状态码
  4. 缺点是安全性,万一假如提供jsonp的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的

Jsonp的实现流程

  1. 声明一个回调函数,把函数名(show)当做参数值
  2. 要传递给跨域请求的数据的服务器,函数形参为要获取目标数据
  3. 创建一个script标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名
  4. 服务器接收到请求后,需要进行处理:把传递的参数名和它需要的数据拼接成一个字符串
  5. 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作

具体代码实现如下:

index.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>
        function jsonp({url,params,cb}){
            return new Promise((resolve,reject)=>{
                let script = document.createElement('script');
                window[cb]=function(data){
                    resolve(data);
                    document.body.removeChild(script);
                }
                params={...params,cb}//wd=b&cb=show
                let arrs = [];
                for(let key in params){
                    arrs.push(`${key}=${params[key]}`);
                }
                script.src = `${url}?${arrs.join('&')}`;
                document.body.appendChild(script)
            })
        }
        //只能发送get请求,不支持post put delete
        //不安全xss攻击  不采用
        jsonp({
            url:'http://localhost:3000/say',
            params:{wd:'早上好'},
            cb:'show'
        }).then(data=>{
            console.log(data)
        })
    </script>
</body>
</html>

serve.js

let express = require('express');
let app = express();

app.get('/say',function (req,res){
    let {wd,cb} = req.query;
    console.log(wd);
    res.end(`${cb}('晚上好')`)
})
app.listen(3000)

注意: 需要安装npm install express, 然后在终端里面输入node serve.js, 再把index.html在浏览器上边console栏查看返回结果

JQuery的jsonp跨域请求

如果从 192.168.19.1ajax请求到 192.168.19.6 会产生跨域问题, 利用jqueryjsonp参数可轻松这个问题。

注意:Jsonp都是GET和异步请求

function get() {
    $.ajax({
           type: "GET",
           url: 'http://192.168.19.6:8080/jsgd/bill.jsp?userCode=?&date='+ new Date(), 
           dataType:"jsonp",
                jsonp:"jsonpcallback",
          success: function(msg){
        $('#callcenter').html(msg.text);
           }
    });
}

cors

什么是cors

cors全称"跨域资源共享"(Cross-origin resource sharing), 是一种ajax跨域请求资源的方式。

兼容性

  1. cors需要浏览器和服务器同时支持,才可以实现跨域的请求
  2. 这个方法几乎所有的浏览器都支持,但是ie必须是10以上
  3. ie8和9需要通过XDomainRequest来实现

请求类型

cors分为简单请求复杂请求两类

简单请求

请求方式使用下列方法之一:

GET
HEAD
POST

Content-Type的值仅限于下列三者之一:

text/plain
multipart/form-data
application/x-www-form-urlencoded

注意:对于简单的请求,浏览器会直接发送cors请求,具体来说就是在header中加入origin请求头字段。在响应头回服务器设置相关的cors请求,响应头字段为允许跨域请求的源。请求时浏览器在请求头的Origin中说明请求的源,服务器收到后发现允许该源跨域请求,则会成功返回。

复杂请求

使用了下面任一HTTP方法

PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH

Content-Type的值不属于下列之一:

application/x-www-form-urlencoded
multipart/form-data
text/plain

当符合复杂请求的条件时,浏览器会自动先发送一个options请求。如果发现浏览器支持该请求,则会将真正的请求发送到后端。如果浏览器发现服务端不支持该请求,则会在控制台抛出错误。

cors字段介绍

  • Access-Control-Allow-Methods

    这个字段是必要的,它的值是逗号分割的一个字符串,表明服务器支持的所有跨域请求的方式

  • Access-Control-Allow-Headers

    如果浏览器请求包括这个字段,则这个字段也是必须的,它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在“预检"中请求的字段

    • Access-Control-Allow-Credentials

      这个字段与简单请求时的含义相同

  • Access-Control-Max-Age

    该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求

流程实现

index.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
</body>
</html>

serve.js

let express = require('express');
let app = express();
app.use(express.static(__dirname));

app.listen(3000)

以当前这个作为静态文件目录,先要在终端里面node serve.js服务器打开,访问localhost:3000就可以把 hello 显示出来。

这是一个完整的复杂请求例子:

index.js

let xhr = new XMLHttpRequest()
document.cookie = 'name=xiaoming'
xhr.withCredentials = true
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiaoming')
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
    console.log(xhr.response)
    //得到响应头,后台需设置Access-Control-Expose-Headers
    console.log(xhr.getResponseHeader('name'))
    }
}
}
xhr.send()

serve.js

let express = require('express');
let app = express();
app.use(express.static(__dirname));

app.listen(3000)

serve2.js

let express = require('express')
let app = express()
let whitList = ['http://localhost:3000']
app.use(function(req, res, next) {
  let origin = req.headers.origin
  if (whitList.includes(origin)) {
    // 设置哪个源可以访问我
    res.setHeader('Access-Control-Allow-Origin', origin)
    // 允许携带哪个头访问我
    res.setHeader('Access-Control-Allow-Headers', 'name')
    // 允许哪个方法访问我
    res.setHeader('Access-Control-Allow-Methods', 'PUT')
    // 允许携带cookie
    res.setHeader('Access-Control-Allow-Credentials', true)
    // 预检的存活时间
    res.setHeader('Access-Control-Max-Age', 6)
    // 允许返回的头
    res.setHeader('Access-Control-Expose-Headers', 'name')
    if (req.method === 'OPTIONS') {
      res.end() // OPTIONS请求不做任何处理
    }
  }
  next()
})
app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.setHeader('name', 'ming') //返回一个响应头,后台需设置
  res.end('早上好')
})
app.get('/getData', function(req, res) {
  console.log(req.headers)
  res.end('早上好')
})
app.use(express.static(__dirname))
app.listen(4000)

Cors与Jsonp比较

  1. cors比Jsonp更强大
  2. Jsonp只支持Get请求,cors支持所有类型的HTTP请求
  3. 使用cors,可以使用XMLHttpRequest发起请求和获取数据,比Jsonp有更好的错误处理
  4. Jsonp的优势在于支持老式浏览器和可以向cors的网络请求数据
  5. cors与Jsonp相比,更方便可靠

postMessage

什么是postMessage

postMessage方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

postMessage语法

otherWindow.postMessage(message, targetOrigin, [transfer])
  • otherWindow:其它窗口(目标窗口)的引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames
  • message:将要发送到其他window的数据,这个数据会自动被序列化,数据格式也不受限制(字符串,对象都可以)
  • targetOrigin:目标窗口的源,可以是字符串*表示无限制,或URL,需要协议端口号和主机都匹配才会发送
  • transfer(可选):是一串和message同时传递的Tranferable对象,这些对象的所有权将 被转移给消息接收方,而发送一方将不再保有所有权

兼容性

高级浏览器Internet Explorer 8+, chromeFirefox , OperaSafari 都将支持这个功能

流程实现

a.html 向 b.html传递 "早上好",然后 a.html 传回"今天天气真好"

a.html

<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe>
<script>
function load(){
    let frame = document.getElementById('frame');
    frame.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>

a.js

let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)

b.js

let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)

Window.name

什么是Window.name

window.name 是一个 window 对象的内置属性,name 属性可以设置或返回存放窗口的名称的一个字符串。

该属性的特征

在页面在浏览器端展示的时候,我们总能在控制台拿到一个全局变量window,该变量有一个name属性,有以下的特征:

  1. 每个窗口都有独立的window.name与之对应
  2. 在一个窗口的生命周期中(被关闭前),窗口载入的所有页面同时共享一个window.name,每个页面window.name都有读写的权限
  3. window.name一直存在与当前窗口,即使是新的页面载入也不会改变window.name的值
  4. window.name可以存储不超过2M的数据,数据个数按需自定义

流程实现

  1. a.html和b.html是同域 http://localhost:3000
  2. c.html是独立的 http://localhost:4000
  3. a获取c的数据
  4. a先引用c
  5. c把值放到window.name,把a引用的地址改为b

a.html

<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
    let first = true
    function load(){
        if(first){
            let iframe = document.getElementById('iframe');
            iframe.src="http://localhost:3000/b.html";
            first = false;
        }else{
            console.log(iframe.contentWindow.name)
            }
        }
<script>

b.html

<body>
    早上好
</body>

c.html

    <script>
        window.name='今天天气不错'
    </script>

a.js

let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)

b.js

let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)

location.hash

什么是location.hash

locationjavascript里面的内置对象,如location.href就管理页面的url,用loaction.href=url就可以直接将页面重定向url,而location.hash则可以用来获取或设置页面的标签值,hash属性是一个可读可写的字符串,该字符串是URL的锚部分(从#号开始的部分)

location.hash的简单应用

#的含义

#代表网页中的位置,其右边的字符,就是该位置的标识符,例如:

http://www.juejin.com/index.html#drafts

就是代表index.html的drafts位置,浏览器读取这个URL后,会自动将print位置滚动至可视区域

HTTP请求不包括#

#是用来指导浏览器的动作的,对服务器端完全无用,所以,HTTP请求中不包括#
例如:

http://www.juejin.com/index.html#drafts

浏览器实际发出的请求是这样的:

GET/index.html HTTP/1.1
Host:www.juejin.com

可以看到,只是请求的index.html,没有#drafts部分

#后的字符

在第一个#出现的任何字符,都会被浏览器解读为位置标识符,这意味着,这些字符不会被发送到服务器端

改变#不触发网页重构

单单改变#后的部分,浏览器只会滚动到相应的位置,不会重新加载网页

改变#会改变浏览器的访问历史

每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用"后退"按钮,就可以回到上一个位置

读取#值

window.location.hash这个属性可读可写。读取时,可以用来判断网页状态是否改变;写入时,则会在不重载网页的前提下,创造一条访问历史记录

onhashchange事件

当#值发生变化时,就会触发这个事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持该事件

Google抓取#的机制

默认情况下,Google的网络忽视URL的#部分

流程实现

路径后面的hash值可以用来通信。目的:a.html 想访问 c.html跨域相互通信。

  1. a.html给c.html传一个hash值,需要通过中间的b.html来实现
  2. c.html收到hash值后 c.html把hash值传递给b.html
  3. b.html将结果放到a.html的hash值中

a.html

<iframe src="http://localhost:4000/c.html#goodmorning"></iframe>
<script>
  window.onhashchange = function () {
    console.log(location.hash);
  }
</script>

b.html

<script>
    window.parent.parent.location.hash = location.hash 
</script>

c.html

<script>
    console.log(location.hash);
    let iframe = document.createElement('iframe');
    iframe.src = 'http://localhost:3000/b.html#goodevening';
    document.body.appendChild(iframe);
</script>

a.js

let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)

b.js

let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)

domain

什么是domain

主要用于主域相同的域之间的数据通信,注意 仅限主域相同,子域不同的跨域应用场景。

实现的原理:两个页面都通过js强制设置 document.domain 为基础主域,就实现了同域

说明

这个方法只能用于二级域名相同的情况下,比如:

www.baidu.com
hhh.baidu.com

这就适用于domain方法

流程实现

a.html

  <iframe src="http://b.ming.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
  <script>
    document.domain = 'ming.cn'
    function load() {
      console.log(frame.contentWindow.a);
    }
  </script>

b.html

<div>
    早上好啊
</div>
<script type="text/javascript">
     document.domain = 'ming.cn'
     var a = 99999;
</script>

a.js

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

b.js

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


这个就可以通过http://a.ming.cn:3000/a.html获取到页面http://a.ming.cn:3000/b.htm中的a的值99999

注意:这里我把我电脑上边的hosts修改了一下,不然不能出来效果

WebSocket

什么是WebSocket

WebSocket是一种网络通信协议,它实现了浏览器与服务器全双工通信,同时允许跨域通讯。原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

WebSocket如何工作

Web浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接。由于 WebSockets 连接长期存在,与典型的HTTP连接不同,对服务器有重要的影响。

注意:基于多线程或多进程的服务器无法适用于WebSockets,因为它旨在打开连接,尽可能快地处理请求,然后关闭连接。任何实际的WebSockets服务器端实现都需要一个异步服务器。

流程实现

a.html

    <script>
    //高级api 不兼容 socket.io(一般使用它)
        let socket = new WebSocket('ws://localhost:3000');
        socket.onopen=function(){
            socket.send('早上好啊')
        }
        socket.onmessage = function(e){
            console.log(e.data);
        }
    </script>

a.js

let express = require('express')
let app = express();
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('今天天气真好')
    })
})


总结

以上就是整理的一些跨域的方法,我觉得一般用cors,jsonp等常见的方法就可以了,不过遇到了一些特殊情况,我们也要做到有很多方法是可以选择的,相信这篇文字会对大家有帮助!

欢迎大家加入❤️❤️❤️
cmd-markdown-logo

参考文章

珠峰架构课


ming
1.9k 声望3k 粉丝

Strive to become better and better