6种跨域解决方案

2,075 阅读7分钟

同源策略

浏览器有同源策略,协议 域名 端口不同,就不能互相访问资源

实现跨域

  • jsonp
  • cors
  • postMessage
  • Window.name
  • http-proxy
  • Document.domain

JSONP

  1. 本地声明一个函数
function show(data){
  console.log(data)
}
  1. 后段返回该函数的执行
<script src="https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=lol&cb=show"></script>
// cb就是厚度返回的执行函数名
//后端会返回
show({
    p: false
    q: "lol"
    s: (10) ["lol官网", "lol手游", "lol转区", "lol是什么意思",
    "lol半价吧", "lol春季赛", "lol转区系统"]
})

封装一个通用jonsp函数

//使用
jsonp({
  url:'xxx',
  params:{wd:'xxx'},
  cb:'xx'
}).then(res=>console.log(data))
let jsonp = ({url,params,cb})=>{
 return new Promise((resolve,reject)=>{
    //1.创建一个script标签
   let script = document.createElement('script')
   //3. 将cb挂载到全局中,后端会返回cb(数据),接收数据返回数据即可
   window[cb]=function(data){
     resolve(data)
   //4. 记得将增加到script删除
     script.remove()
   }
   //2.将params和cb转化成wd=xxx&cb=xxx
   params = {...params,cb}
   let arr = []
   for(let key in params){
     arr.push(`${key}=${params[key]}`)
   }
   script.src=url+'?'+arr.join('&')
   document.body.appendChild(script)
 })
}

jsonp的缺点

  1. 只能发送get请求,不支持post put delete
  2. 不安全,引用链接的网站如果返回了一段攻击代码,可能造成危害,xss攻击,不支持

node模拟下提供jsonp功能

let express = require('express')
let app = new express()
app.get('/say',function(req,res){
	let {wd,cb}=req.query
	res.end(`${cb}('返回的数据')`)
})
app.listen(3000,function(){console.log('启动')})

cors

这是一个后端的解决方案,可以用node来模拟

image-20200506115345498

启动两个服务器,一个3000端口,一个4000端口

3000端口启动一个网页,在网页中访问4000端口的资源

const express = require('express');
let app = express();
//将这个目录下的资源以静态资源的方式提供访问
app.use(express.static(__dirname));
app.listen(3000, function () {
	console.log('3000启动');
});
// index.html
<script>
  let xhr = new XMLHttpRequest();
  xhr.open('get', 'http://localhost:4000/say');
  xhr.onreadystatechange = function () {
    if(xhr.readyState === 4 && 300 >= xhr.status&&xhr.status >= 200) {
      console.log(xhr.response);
    }
  };
  xhr.send();
</script>

浏览器输入:localhost:3000/index.html,会警告跨域问题

image-20200506115939888

并且端口:4000的服务是有接受到端口3000的请求的,只是返回的内容被浏览器屏蔽了

image-20200506120309199

提示:Access-Control-Allow-Origin没有允许http://localhost:3000访问资源,可以

设置允许访问的域名

//设置允许访问的域名的白名单
let whiteList = ['http://localhost:3000'];
app.use(function (req, res, next) {
  //获取请求的origin
    let origin = req.headers.origin;
    if (whiteList.includes(origin)) {
    //如果在白名单内,则允许他访问
        res.setHeader('Access-Control-Allow-Origin', origin);
    }
    next();
});

设置允许访问的请求方式

但这种方式只允许get,post,head这种简单的请求,如果是put呢

xhr.open('put','http://localhost:3000/say')
app.put('/say',function(req,res){
  res.end('post请求')
})

image-20200506122225939

浏览器报错,我们需要后端设置允许put请求

app.use(function(req,res){
    res.setHeader('Access-Control-Allow-Methods','PUT')//记得大写
})

设置需要允许访问的请求头

如果我们在请求头中加点信息呢,

xhr.setRequestHeader('name','huzhiwu')

浏览器警告说,需要后端允许那个请求头

image-20200506123535070

但端口4000能接受到这个请求头

屏幕快照 2020-05-06 下午12.37.02

设置允许的请求头

res.setHeaders('Access-Control-Allow-Headers','name')

设置max-age

我们在端口4000的服务中,打印每次请求的method

app.use(function(req,res){
	console.log(req.method)
})

刷新浏览器时,会发现。端口4000打印了两个method

image-20200506125901401

options是浏览器在发现跨域时,会先向服务器一个预检请求,该方法返回允许访问的methods,如果请求方法不在methods中则报错,存在的话,就发送真正的请求

这样子每次发送一个请求,实际发送了两个请求,是比较耗性能的,如果我们的方法不经常改变,可以让

浏览器在一段时间内预检一次就够了

req.setHeader('Access-Control-Max-Age',5)//单位秒

允许跨域携带cookie

let xhr = new XMLHttpRequest();
xhr.open('put', 'http://localhost:4000/say');
document.cookie='name=huzhiwu'
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && 300 >= xhr.status && xhr.status >= 200) {
  	console.log(xhr.response);
  }
};
xhr.send();
// 端口4000的服务器
app.use(function(req,res){
  console.log(req.headers)
})

image-20200506131123492

发现没有我们携带的cookie

在ajax请求中增加

xhr.withCredentials = true;

浏览器又报错了

image-20200506131343155

在node中设置允许跨域携带cookie即可

res.serHeader('Access-Control-Allow-Credentials')

允许浏览器获取服务器返回的headers

app.put('/say',function(req,res){
  res.setHeader('name','huzhiwu')
  res.end('put')
})
let xhr = new XMLHttpRequest();
xhr.open('put', 'http://localhost:4000/say');
xhr.onreadystatechange = function () {
	if (xhr.readyState === 4 && 300 >= xhr.status && xhr.status >= 200) {
		console.log(xhr.getResponseHeader('name'));
	}
};
xhr.send();

浏览器又报错

image-20200506131909812

浏览器觉得服务器返回给你的请求头不安全,不给你获取

在node中设置

res.setHeader('Access-Control-Expose-Headers','name')

小结

//允许哪源可以访问我
res.setHeader('Access-Control-Allow-Origin',origin)
//允许那个方法可以访问我
res.setHeader('Access-Control-Allow-Methods','PUT')
//预检存活时间
res.setHeader('Access-Control-Max-Age',5)//单位秒
//允许请求携带的headers
res.setHeader('Access-Control-Allow-Headers','name')
//允许浏览器获取服务器返回的headers
res.setHeader('Access-Control-Expose-Headers','name')

postMessage跨域

页面跟嵌套在iframe中的页面通信

image-20200506140707910

分别开启a,b两个服务,端口号分别是3000,4000

//a.js b.js
const express = require('express');
const app = express();
app.use(express.static(__dirname));
app.listen(4000/3000, function () {
	console.log('4000/3000启动');
});

a页面放在3000端口,b页面放在4000端口

在b页面中引用a页面

<body>
  <iframe src='http://localhost:3000/a.html' id='aIframe' onload='load()'>
  </iframe>
  <script>
  	function load(){
  	    let frame = document.getElementById('aIframe')
  	    //给嵌套在iframe中的页面的window发送信息
  	    //第二个参数是origin
  	    frame.contentWindow.postMessage('你好我是b页面','http://localhost:3000/a.html')
    }
    //b页面监听来自a页面的信息
    window.onmessage=function(e){
      console.log(e.data)
    }
  </script>
</body>

在a页面中监听b页面的信息

window.onmessage=function(e){
  console.log(e.data)
  //收到信息后给b页面发送信息,
  e.source.postMessage('你好,我是a页面',e.origin)
}

浏览器打开http://localhost:4000/b.html

image-20200506141444334

小结

发送信息

window.postMessage('信息',orign)

接收信息

window.onmessage=function(e){
  console.log(e.data)
}

window.name跨域

image-20200506142309227

window.name默认为空字符串,我们可以在name中存入信息,跨域访问他

  1. a和b是同域的http://localhost:3000
  2. c是另一个域的http://localhost:4000
  3. a页面先iframe引用c,再iframe引用b,
  4. a页面即可获取c页面的window.name

image-20200506142808012

开启两个服务器,分别是3000和4000端口

c页面

<script>
	window.name='我是c页面'  
</sctript>

a页面

<body>
  <iframe src='http://localhost:4000/c.html' id='frame' onload='load()'>
  </iframe>
  <script>
    let first=true
    let frame = document.getElementById('frame')
    function load(){
  	//第一次加载完成,立即将frame的src转到同源下
  	if(first){
  	    frame.src='http://localhost:3000/b.html'
  	    first=false;
  	}else{
  	    //同源下,即可访问name
  	    console.log(frame.contentWindow.name)
  	}
        
    }
  </script>
</body>

image-20200506143706273

http-proxy

当网络请求跨域时,可以使用代理。

服务器访问服务器没有跨域问题.所以,我们的做法是利用中间的代理浏览器向目标浏览器发请求。

image-20200506160700384

  1. 分别开启3000端口和4000端口的服务
  2. a页面放在3000端口中
  3. a页面发送请求,3000端口的服务器转发这个请求到4000端口
a.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
//将请求转发到另一个服务器
const apiProxy = createProxyMiddleware('/say', {
	target: 'http://localhost:4000',
});

const app = express();
app.use(express.static(__dirname));
app.use(apiProxy);
app.listen(3000, function () {
	console.log('3000启动');
});
b.js
const express = require('express');
const app = express();
app.use(express.static(__dirname));
app.get('/say', function (req, res) {
	res.end('我是b服务器的信息');
});
app.listen(4000, function () {
	console.log('4000启动');
});
<script>
    let xhr = new XMLHttpRequest();
    xhr.open('get', 'http://localhost:3000/say');
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && 300 >= xhr.status && xhr.status >= 200) {
            console.log(xhr.response);
        }
    };
    xhr.send();
</script>

image-20200506161055860

http-proxy-middleware的更多参数请看https://github.com/chimurai/http-proxy-middleware

document.domain跨域

举个例子www.video.baidu.com和www.map.baidu.com是两个二级域名,

// www.map.baidu.com
<body>
  <script>
  	window.name='huzhiwu'
  </script>
</body>
// www.video.baidu.com
<body>
  <iframe src='www.map.baidu.com' id='frame' onload='load'>
  </iframe>
  <script>
  function load(){
      let frame = document.getElementById('frame')
      console.log(frame.contentWindow.name)//跨域访问不到
  }
  </script>
</body>

因为跨域访问不到,

但只要在两个二级域名中加个document.domain='baidu.com'即可互相访问

代码

github.com/huzhiwu1/Sa…

结语

作者:胡志武

时间:2020/05/06

如果喜欢的话,请点个赞吧,如果有错漏处,请指正