啰啰嗦嗦的跨域问题

818 阅读8分钟

文章开始之前

之前在《不要再问我跨域的问题了》看了这个跨域问题,写的通俗易懂,我开开心心的就看完了,好像真的不怕人家问跨域了的样子,本来想开开心心写博客了,但是看到评论我就突然就懵了。

该评论提到:"但是CSRF攻击不是因为同源策略所以无法发动攻击的吧",emm...好像有点道理,但是好像又有哪里不对,然后我想啊,想啊,想啊,想到了今天,终于茅塞顿开,想明白了。好像评论说的还挺对...

好吧,相信你看我碎碎念这么久也看不懂我在说啥,我先说说我理解的CSRF攻击吧~贴上大佬的文章《浅谈CSRF攻击方式》

有个问题我想了很久,你说CSRF攻击会被同源策略限制吧,那他一个src属性怎么带上你的cookie的?他又不长腿,不可能自己跨域拿到别人的cookie吧...后来经过百度(资料少得很,几乎所有博客都是复制粘贴的哼),然后自己猜测,然后问了老师。得到了下面的结论:

首先要弄清楚一个问题,同源策略是浏览器给的,cookie也是保存在浏览器的,访问网站的时候带上cookie也是浏览器的操作。那么,浏览器带上什么cookie,主要看你访问的是什么网站,然后才要访问的地址和cookie的域名去匹配。

引用一下别人的资料:

浏览器根据访问的服务器地址来决定发送哪个cookie,首先主机(域名或者ip)必须一致。这个服务器的地址的父级路径(不含主机)作为路径值和cookie的路径进行匹配,如果服务器地址的路径可以和cookie的路径完全匹配,则将这个cookie发送给服务器。所以服务器的某个页面想要拿到某个Cookie,这个页面的路径必须和cookie路径相同或者是他的子路径。

需要注意不同浏览器规定不一样,对于chrome,当前页面只能访问当前目录及其以上的Cookie。

反正不管你这个页面打开还是关闭,只要浏览器还存着你的cookie,而且你的cookie还没失效,那浏览器就能发送你的cookie。

举例子来说:如果你登陆了银行网站www.bank.com,然后又在浏览器内点开一个坏网站www.bad.com,没想到这个坏网站居然跑去请求你登录着的银行网站www.bank.com!浏览器一看:"哦?又是访问这银行网站的,刚好我这里有cookie耶嘿嘿嘿,呐给你cookie,去吧去吧~"。然后坏网站就愉快的携带着www.bank.com给你的cookie去为所欲为了。这个是CSRF攻击的原理。

那我们再说说同源策略,同源策略的限制有三点,其中一点如下:

无法用js读取非同源的cookie、LocalStorage 和 IndexDB 无法读取。

诶?js不能读取非同源的cookie啊,那是怎么拿到www.bank.com给你的cookie的呀?注意哦,他完全不需要读取cookie,他只是需要访问www.bank.com这个地址,蠢蠢的浏览器就自动给他戴上cookie,gogogo了。他不看从哪个页面发出的请求,他只看你最终的地址。

看到这里,你大概就能明白我的观点了吧,CSRF攻击并不需要读取cookie,所以他是不会被同源限制的。

好啦,接下来正式讲,为什么会有跨域问题?

为什么会有跨域问题?

对于跨域问题,有个看的半懂不懂的一个定义:"同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。" 害,还不是为了安全嘛。

那他到底安全在哪里哦?

首先,他有三点限制:参考文章《浏览器同源策略是什么?没有同源策略会怎么样?》

  1. 无法用js读取非同源的Cookie、LocalStorage和IndexDB。

    也就是无法读取缓存对吧~这个还是挺好理解的,要是能随随便便读取然后操作你的cookie,那谁还做网上银行呀..

    其他两个缓存也是,有些网站用缓存来放购物车信息啥的,虽然是不太重要的信息,但是也不能被人家乱搞对吧~

  2. 无法用js获取非同源的DOM。

    如果没有这一条,恶意网站可以通过iframe打开银行页面来获取dom,就相当于可以获取整个银行页面的信息。我参考的那个文章有个段子例子挺好玩的,我复制过来一下。

    有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进www.yinghang.com改密码。你吓尿了,赶紧点进去,还是熟悉的银行登录界面,你果断输入你的账号密码,登录进去看看钱有没有少了。睡眼朦胧的你没看清楚,平时访问的银行网站是www.yinhang.com,而现在访问的是www.yinghang.com,这个钓鱼网站做了什么呢?

    <iframe name="yinhang" src="www.yinhang.com"></iframe>
    
    // 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
    const iframe = window.frames['yinhang']
    const node = iframe.document.getElementById('你输入账号密码的Input')
    console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)
    

    还有就是,用form表单提交到不同源的网页是被允许的,因为form提交到另一个域名之后,原页面的脚本无法获取新页面中的内容,所以浏览器认为这是安全的。

  3. 无法用js发送非同源的AJAX请求。《AJAX跨域访问被禁止的原因》

    AJAX是干嘛哒?是为了和我们的服务端同志通信的,得到服务端的信息的。如果到了生产环境,我们用AJAX访问自己的服务端,是不会有跨域问题的对吧,因为都在同一个域名下。(当然我们平时开发的时候是在自己的电脑上开发的,跨域问题肯定有的)

    那如果我们可以通过AJAX访问非同源的网站,比如说淘宝。那还搬什么砖啊(摔)!直接写个AJAX过去,让淘宝客户端给我们返回用户隐私数据,然后我们再把这些数据拿出去卖....嘿嘿...

    好了,梦到这里就结束了,继续学习吧。

解决跨域问题

真是一个俗气但是又让人困扰的问题啊...由于我的nodejs忘得差不多了,导致今天写的代码乱七八糟,所以我就直接用别人的代码了哈。在这里跟大家说,看代码是不够的,一定要自己尝试一下跨域哦~

JSONP

毕竟也不是所有资源都不能被跨域访问的嘛,下面两种交互是不会被同源策略限制的:

  1. 跨域写操作(Cross-origin writes)

    例如超链接、重定向以及表单的提交操作,特定少数的HTTP请求需要添加预检请求。

  2. 跨域资源嵌入(Cross-origin embedding)

    1. <script> 标签嵌入的跨域脚本
    2. <link> 标签嵌入的 CSS 文件
    3. <img> 标签嵌入图片
    4. <video> 和 <audio> 标签嵌入多媒体资源
    5. <object>, <embed>, <applet> 的插件
    6. @font-face 引入的字体,一些浏览器允许跨域字体,一些需要同源字体。
    7. <frame>和<iframe>载入的任何资源,站点可以使用X-FrameOptions消息头来组织这种形式的跨域交互。

src属性的资源都是可以被跨域请求的,href资源大部分也都是可以被跨域请求的。所以我们动动手脚..利用src标签进行请求。

但是为什么使用script标签呢,因为script标签请求回来的内容会被当做JS代码来执行,所以我们要求后端返回一个执行方法的字符串如"callback(响应结果)",然后我们在自己的js代码中定义function callback(res){...}就可以拿到响应结果啦。

// nodejs 后端接口
// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
  static async jsonp (ctx) {
    // 前端传过来的参数
    const query = ctx.request.query
    // 设置一个cookies
    ctx.cookies.set('tokenId', '1')
    // query.cb是前后端约定的方法名字
    // 其实就是后端返回一个直接执行的方法给前端
    // 由于前端是用script标签发起的请求,所以返回了这个方法后相当于立马执行,并且把要返回的数据放在方法的参数里。
    ctx.body = `${query.cb}(${JSON.stringify(successBody({msg: query.msg}, 'success'))})`
  }
}
module.exports = CrossDomain
<!--前端代码-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script type='text/javascript'>
      // 后端返回直接执行的方法,相当于执行这个方法,由于后端把返回的数据放在方法的参数里,所以这里能拿到res。
      window.jsonpCb = function (res) {
        console.log(res)
      }
    </script>
    <script src='http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb' type='text/javascript'></script>
  </body>
</html>

实现封装:

const request = ({url, data}) => {
  return new Promise((resolve, reject) => {
    // 处理传参成xx=yy&aa=bb的形式
    const handleData = (data) => {
      const keys = Object.keys(data)
      const keysLen = keys.length
      return keys.reduce((pre, cur, index) => {
        const value = data[cur]
        const flag = index !== keysLen - 1 ? '&' : ''
        return `${pre}${cur}=${value}${flag}`
      }, '')
    }
    // 动态创建script标签
    const script = document.createElement('script')
    // 接口返回的数据获取
    window.jsonpCb = (res) => {
      document.body.removeChild(script)
      delete window.jsonpCb
      resolve(res)
    }
    script.src = `${url}?${handleData(data)}&cb=jsonpCb`
    document.body.appendChild(script)
  })
}
// 使用方式
request({
  url: 'http://localhost:9871/api/jsonp',
  data: {
    // 传参
    msg: 'helloJsonp'
  }
}).then(res => {
  console.log(res)
})

使用JSONP的时候有两点要注意的:

  1. 只能发送get请求,因为请求js资源的时候用的就是get请求。
  2. 在jquery中使用jsonp发送请求的时候,jquery会先判断是否同源。如果同源的话就假装自己不是jsonp,设置的是get就用get发送请求,设置的是post就用post发送请求。但是如果不同源,无论设置为什么请求方式,都会用get请求。

空iframe加form

刚才那个只能发get请求,现在能打的来了!我们可以利用form发post请求。其实好像很久之前都是用post发送请求的,上面也说了,form提交到另一个域名之后,会跳转到新页面或者刷新我们的页面,而原页面的脚本无法获取新页面中的内容,所以浏览器认为这是安全的。

想要实现无刷新的请求呢,就要跳转打开新的页面。那么我们又不想开一个新页面,那咋办嘛!当然是使用iframe啦~我们可以在表单提交成功之后,把返回的内容放到另一个页面F中显示,但是这个页面F又作为框架放在当前页面中,这样就实现了页面不会发生跳转也不会刷新。

要注意的是,这种方式拿不到返回值orz...因为他直接把返回值显示在iframe中了,我们无法获取iframe里面的dom,而且,这个iframe一般都是被隐藏的。

上代码!

// nodejs 后端接口
// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
  static async iframePost (ctx) {
    let postData = ctx.request.body
    console.log(postData)
    ctx.body = successBody({postData: postData}, 'success')
  }
}
module.exports = CrossDomain
// 前端代码
const requestPost = ({url, data}) => {
  // 首先创建一个用来发送数据的iframe.
  const iframe = document.createElement('iframe')
  iframe.name = 'iframePost'
  iframe.style.display = 'none'
  document.body.appendChild(iframe)
  const form = document.createElement('form')
  const node = document.createElement('input')
  // 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话.
  iframe.addEventListener('load', function () {
    console.log('post success')
  })

  form.action = url
  // 在指定的iframe中执行form
  form.target = iframe.name
  form.method = 'post'
  for (let name in data) {
    node.name = name
    node.value = data[name].toString()
    form.appendChild(node.cloneNode())
  }
  // 表单元素需要添加到主文档中.
  form.style.display = 'none'
  document.body.appendChild(form)
  form.submit()

  // 表单提交后,就可以删除这个表单,不影响下次的数据发送.
  document.body.removeChild(form)
}
// 使用方式
requestPost({
  url: 'http://localhost:9871/api/iframePost',
  data: {
    msg: 'helloIframePost'
  }
})

CORS

CORS是一个W3C标准,是处理跨域问题的标准做法。他的实现主要是在服务端,通过HTTP Header来限制可以访问的域。比如说页面A想要访问B服务器,如果B服务器上声明了:允许A的域名进行访问,那么从A到B的跨域请求就可以完成。

他将请求分为两种:简单请求和非简单请求。

简单请求
  1. 请求方法是以下三种方法之一:
    1. HEAD
    2. GET
    3. POST
  2. HTTP的头信息不超出以下几种字段:
    1. Accept
    2. Accept-Language
    3. Content-Language
    4. Last-Event-ID
    5. Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

对于简单请求,代码如下:

// nodejs后端代码
// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
  static async cors (ctx) {
    const query = ctx.request.query
    // 设置为*时cookie不会在http请求中带上
    ctx.set('Access-Control-Allow-Origin', '*')
    ctx.cookies.set('tokenId', '2')
    ctx.body = successBody({msg: query.msg}, 'success')
  }
}
module.exports = CrossDomain

Access-Control-Allow-Origin:指定了允许访问该资源的外域URI。它可以是一个origin值,也可以是*,表示接受任意域名的请求。

对于简单请求,前端就正常发请求就可以了。

fetch(`http://localhost:9871/api/cors?msg=helloCors`).then(res => {
  console.log(res)
})

当浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。如下:(参考文章:《跨域资源共享 CORS 详解》

GET /cors HTTP/1.1
//本次协议来自哪里,服务器可以根据这个信息判断是否同意这次请求
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

如果这个请求的url不在许可范围之内,服务器还是会返回一个正常的HTTP回应,然后浏览器拿到之后会发现少了点什么(Access-Control-Allow-Origin)字段,就会发现出错了,然后抛出错误,这个错误能被XMLHttpRequestonerror回调函数捕获。要注意这时候状态码可能是200,所以这个错误无法通过状态码来识别。

那如果在许可范围之内,服务器的响应头就会多几个字段:

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

几个字段的对应信息如下:

  1. Access-Control-Request-Headers:可选,用于指定除了基本字段以外的字段。比如上面的例子中,getResponseHeader('FooBar')可以返回FooBar字段的值。如果不指定其他字段,getResponseHeader()方法就只能拿到基本字段的值。

  2. Access-Control-Allow-Credentials:可选,表示服务器是否允许发送Cookie,这时候前端也要设置credentials(告诉浏览器同意发送)。默认情况下,Cookie不包括在CORS请求之中。

    要注意的是如果设置为true,Access-Control-Allow-Origin就不能设为星号,必须指定与请求网页一致的域名,这时候依然遵守同源策略,只有用这个域名设置的Cookie才会上传。

非简单请求

还有一些会对服务器有特殊要求的请求,被称为非简单请求。比如请求方法是PUT或者是DELETE,或者content-type字段的类型是application/json。对于这种请求,服务器会使用OPTIONS方法来发起预检请求,看看服务器允不允许跨域请求,允许哪些请求方法,允许哪些头字段,预检请求的返回码是204。也可以在返回信息中告诉客户端需不需要身份认证信息。如果服务器允许,就会发起实际的请求,请求成功之后返回200。

举例一个预检请求的HTTP头信息:

OPTIONS /cors HTTP/1.1
//浏览器自动加的字段
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

这个是预检请求的时候携带的东西,有两个特殊字段:

  1. Access-Control-Request-Method:必须,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT
  2. Access-Control-Request-Headers:指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header

服务器收到预检请求之后,回应如下:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
//表示http://api.bob.com可以请求数据 也可以设置为*
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

和简单请求一样,就算否定了预检请求,也会返回一个正常的HTTP回应,而没有CORS相关的头信息字段。然后浏览器发现少了东西就会触发错误。

服务器回应的其他几个字段:

  1. Access-Control-Request-Method:表明服务器支持的所有跨域请求的方法。注意是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
  2. Access-Control-Request-Headers:表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。(如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。)
  3. Access-Control-Max-Age:可选,指定本次预检请求的有效期,单位是秒。1728000表示20天。

通过预检请求之后的请求和回应就和简单请求一样啦。

代码:

// 后端node.js
// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
  static async cors (ctx) {
    const query = ctx.request.query
    // 后端还是要设置指定的origin
    ctx.set('Access-Control-Allow-Origin', 'http://localhost:9099')
    // 如果需要http请求中带上cookie,需要前后端都设置credentials
    ctx.set('Access-Control-Allow-Credentials', true)
    ctx.set('Access-Control-Request-Method', 'PUT,POST,GET,DELETE,OPTIONS')
    ctx.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, t')
    ctx.cookies.set('tokenId', '2')

    ctx.body = successBody({msg: query.msg}, 'success')
  }
}
module.exports = CrossDomain

前端代码:

fetch(`http://localhost:9871/api/cors?msg=helloCors`, {
  // 需要带上cookie
  credentials: 'include',
  // 这里添加额外的headers来触发非简单请求
  headers: {
    't': 'extra headers'
  }
}).then(res => {
  console.log(res)
})

与JSONP比较:JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

代理

同源策略只是浏览器的,后端服务器去请求地址的时候可没有同源策略呀。浏览器不允许的话那我就用服务器去帮我请求嘛。于是就出现了服务器代理。

好啦,那我们来改写代码:要在前端的服务器上写下面的代码,作为代理服务器哦,注意不是后端服务器!!

// node.js前端服务器
const express = require('express'); 
const app = express(); 
const Axios = require('axios'); 
 
app.use(express.static('page')) 
app.get('/getInfo', (req, res) => {    
    Axios.get('http://localhost:8081/getInfo').then(data => {         
        res.end(data.data)     
    }).catch(err => console.log(err)) 
}) 
 
app.listen(8080); 

如上,当前端服务器localhost:8080收到请求的时候,请求后端服务器localhost:8081,相当于完成了一次请求的转发。

前端页面正常请求即可:

fetch(`http://localhost:8080/getInfo`).then(res => {
  console.log(res)
})

Nginx代理

其实是差不多的道理,但是我们那个是自己手写的,功能当然比不上人家Nginx啦。Nginx是一款自由的、开源的、高性能的HTTP服务器和反向代理服务器,可以作为一个HTTP服务器进行网站的发布处理,可以作为反向代理实现负载均衡的实现。emm...对于这段话,知道人家是一个很厉害的服务器就好了,对于什么是反向代理,什么是负载均衡,下面慢慢讲。

Nginx的代理模式分为正向代理和反向代理,参考文章《Nginx详解(正向代理、反向代理、负载均衡原理)》

首先说说什么是正向代理:

正向代理最大的特点就是我们很明确自己要访问的服务器地址(www.fuwu.com),但是服务器只知道这个请求是代理服务器(www.daili.com)发给他的,并不知道是我们发起的。

具体的场景就是:当我们某天需要国外的某些网站的时候,不是要使用那啥(什么梯什么子)..嘛。他主要是找到一个可以访问国外的网站的代理服务器,然后把我们的请求转发到代理服务器那里,代理服务器去请求,然后把请求结果交给我们。

你看我们不是很明确自己要去的网站的地址嘛,但是由于是代理服务器帮我们转发的请求,所以国外的服务器并不知道是我们在请求,他只知道是代理服务器发过来的请求。

好了,那什么是反向代理呢?

其实反向代理就是我们刚才看到的那种模式,什么模式呢?就是我客户端直接就说了我要请求代理服务器(www.daili.com),但是转发到什么地方由代理服务器来决定,也就是说,反向代理会隐藏服务器的信息。

举个例子吧,比如说淘宝,每天这么多人访问淘宝,他总不可能只用一台服务器吧。这时候就出现了"分布式部署",也就是说用多台服务器去解决请求。据说淘宝的大部分功能也是用nginx反向代理实现的,而且他自己又封装了nginx和其他组件,起名为Tengine,有兴趣的童鞋可以访问Tengine的官网查看具体的信息:tengine.taobao.org/。

那这种情况怎么利用反向代理呢?首先nginx收到客户端发送的请求之后,按照一定的规则分发给后台服务器。我们只知道我们请求了代理服务器(www.daili.com),但是不知道最后是哪一个服务器处理了我们的请求。

其实在很多时候,正向代理和反向代理有可能存在于一个应用场景中,如图:

img

正向代理(www.zheng.com),能代理客户端去访问反向代理服务器(www.fan.com)。然后反向代理(www.fan.com)又根据规则把请求分发给真正处理业务的服务器(www.yewu.com)。

什么是负载均衡呢?

什么是负载?反向代理服务器接收到的请求数量就是负载。其实我觉得还挺形象的吧哈哈哈,感觉好像是在说反向代理服务器上背负了很多的请求,要把这些请求都分发给业务服务器。

那什么是均衡呢?反向代理服务器把请求按照规则进行分发,这些规则就是均衡规则。

那什么是负载均衡呢?就是反向代理服务器把接收到的请求数量按照规则进行分发。

nginx支持的负载均衡分配算法方式如下:

  1. weight轮询(默认)

    weight是权重的意思。接收到的请求按照顺序分配给业务服务器,如果有哪个服务器挂了就踢出去,不发给他。这种方式下,我们可以给不同的后端服务器设置权重值,调整请求的分配率,权重大的,分配到的请求越多。这个权重值主要是根据后端服务器的硬件配置来调整的。

  2. ip_hash

    把请求按照ip的hash结果匹配,这样的话一个ip总会访问到同一个服务器。也在一定程度上解决了session共享的问题。

  3. fair

    智能调整调度算法,可以动态的根据后端服务器的请求处理到响应时间来均衡分配。处理效率高的当然被分配的概率就高一点啦。但是需要注意的是nginx默认不支持fair算法,如果要使用这种调度算法,请安装upstream_fair模块。

  4. url_hash

    按照访问的url的hash结果来分配请求,每个请求的url都会指向后端固定的某个服务器。nginx默认不支持这种调度算法,要使用的话需要安装nginx的hash软件包。

简单的使用一下:

# Nigix配置
server{
    # 监听9099端口
    listen 9099;
    # 域名是localhost
    server_name localhost;
    #凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871 
    location ^~ /api {
        proxy_pass http://localhost:9871;
    }    
}

前端还是正常发送请求

//前端的端口域名事域名http://localhost:9099,所以要用9099端口发起请求才不会跨域
fetch('http://localhost:9099/api/iframePost', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    msg: 'helloIframePost'
  })
})

跨域进行DOM查询

postMessage

HTML5提供的接口,可以实现不同窗口不同页面之间的跨域通讯。也就是说不需要后端,我们自己就可以搞定啦。例子如下:

这里是http://localhost:9099/#/crossDomain,发消息方

<template>
  <div>
    <button @click="postMessage">给http://crossDomain.com:9099发消息</button>
    <!--内嵌iframe框架-->
    <iframe name="crossDomainIframe" src="http://crossdomain.com:9099"></iframe>
  </div>
</template>

<script>
export default {
  mounted () {
    //监听回复
    window.addEventListener('message', (e) => {
      // 这里一定要对来源做校验
      if (e.origin === 'http://crossdomain.com:9099') {
        // 来自http://crossdomain.com:9099的结果回复
        console.log(e.data)
      }
    })
  },
  methods: {
    postMessage () {
      const iframe = window.frames['crossDomainIframe']
      //发送消息
      iframe.postMessage('我是[http://localhost:9099], 麻烦你查一下你那边有没有id为app的Dom', 'http://crossdomain.com:9099')
    }
  }
}
</script>

这里是crossdomain.com:9099,接收消息方

<template>
  <div>
    我是http://crossdomain.com:9099
  </div>
</template>

<script>
export default {
  mounted () {
    //监听发过来的消息
    window.addEventListener('message', (e) => {
      if (e.origin === 'http://localhost:9099') {
        // http://localhost:9099发来的信息
        console.log(e.data)
        // e.source可以是回信的对象,其实就是http://localhost:9099的window
        // e.origin是事件源对象,也就是当前窗口
        e.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟,这就是你想知道的结果:${document.getElementById('app') ? '有id为app的Dom' : '没有id为app的Dom'}`, e.origin);
      }
    })
  }
}
</script>

document.domain

这种方法只适合主域名相同但是子域名不相同的iframe跨域。

比如主域名是crossdomain.com:9099,子域名是child.crossdomain.com:9099,然后我们给两个页面指定:document.domain = crossdomain.com,他们就可以访问对方的window对象啦。

canvas操作图片的跨域

参考文章:《解决canvas图片getImageData,toDataURL跨域问题》有兴趣的还是看张鑫旭大佬的原文吧哈哈哈哈,这里写的很简短,主要是写给我复习用的。

场景:有些团队会用一个专门的域名去放置静态资源,但是我们的主页面所在的域名往往和静态资源的域名不一样,如果需要获取图片的详细信息什么的(例如getImageData()方法获取图片的完整的像素信息,toDataURL()方法返回一个包含图片展示的data URI)就会出现跨域问题了。

首先最简单的一步:我们需要配置图片服务器的请求头为*或者是指定的域名,这时候就不会出现Access-Control-Allow-Origin相关的错误信息了,但是还会有其他的跨域错误信息。

举例:前端代码

var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');

var img = new Image();
img.onload = function () {
    context.drawImage(this, 0, 0);
    context.getImageData(0, 0, this.width, this.height);
};
img.src = 'https://avatars3.githubusercontent.com/u/496048?s=120&v=4';

依然会报错,而且各浏览器报错内容也不一致,这时候可以试试crossOrigin。在HTML5中,有些元素(<img>,<video>,<script>)提供了支持CORS的属性crossOrigin。所以我们可以把上面的代码变成这样:

var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');

var img = new Image();
//只加了这一句话
img.crossOrigin = '';
img.onload = function () {
    context.drawImage(this, 0, 0);
    context.getImageData(0, 0, this.width, this.height);
};
img.src = 'https://avatars3.githubusercontent.com/u/496048?s=120&v=4';';

把crossOrigin设置为空字符串的时候,其实设置的是"anonymous"。还有另外一个值是"use-credentials",只要设置的不是"use-credentials"统统被解析成"anonymous"。

  1. anonymous:表示元素的跨域资源请求不需要凭证
  2. use-credentials:请求需要凭证,我们要在请求中提供凭证

为啥这个属性可以解决问题呢?因为它相当于告诉对方的服务器:我不需要别的东西,我就要你这几张图!然后浏览器就觉得听起来还挺老实的,图片给你给你。

兼容性问题

文章里面说IE11+(IE Edge),Safari,Chrome,Firefox浏览器均支持,IE9和IE10会报SecurityError安全错误,然后我去caniuse查了一下,好像还是这个样子。那么我们就要解决问题了!

这时候就要用ajax来跨域了。

var xhr = new XMLHttpRequest();
xhr.onload = function () {
    //会创建一个DOMString 其实DOMString就是String 里面是一个指向参数的url
    //其实就是创建一个图片的url
    var url = URL.createObjectURL(this.response);
    var img = new Image();
    img.onload = function () {
        // 此时你就可以使用canvas对img为所欲为了
        // ... code ...
        // 当不再需要这些URL对象时,必须通过调用URL.revokeObjectURL()方法来释放
        // 浏览器会在文档退出的时候自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。
        URL.revokeObjectURL(url);
    };
    img.src = url;
};
xhr.open('GET', url, true);
//类似文件对象的原始数据 听说也可以设置为arraybuffer?没试过,下次试一试。
xhr.responseType = 'blob';
xhr.send();

根据实践发现,在IE浏览器下,如果请求的图片过大,几千像素那种,图片会加载失败,可能是超过blob尺寸限制。

参考文章

  1. AJAX跨域访问被禁止的原因

感叹一下:今天也是被大佬虐的一天,唉,睡觉。