跨域

前言

关于跨域,自己看了很多博客,但总是有一种无法贯通的感觉,今天自己总结一下,希望能更好的理解。

正文

1.什么是跨域
首先要了解同源和跨源的概念,同源的定义:如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。
三个任一不相同 则为跨源。
跨域不一定是浏览器限制了发起跨站请求,而也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是 CSRF 跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有些浏览器不允许从 HTTPS 的域跨域访问 HTTP,比如 Chrome 和 Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。

2.为什么要有同源策略
MDN上:同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。据我了解,浏览器是从两个方面去做这个同源策略的,一是针对接口的请求,二是针对Dom的查询。试想一下没有这样的限制上述两种动作有什么危险。

1.没有同源策略限制的接口请求、
有一个小小的东西叫cookie大家应该知道,一般用来处理登录等场景,目的是让服务端知道谁发出的这次请求。如果你请求了接口进行登录,服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中,服务端就能知道这个用户已经登录过了。知道这个之后,我们来看场景:
1.你准备去清空你的购物车,于是打开了买买买网站www.maimaimai.com,然后登录成功,一看,购物车东西这么少,不行,还得买多点。
2.你在看有什么东西买的过程中,你的好基友发给你一个链接www.nidongde.com,一脸yin笑地跟你说:“你懂的”,你毫不犹豫打开了。
3.你饶有兴致地浏览着www.nidongde.com,谁知这个网站暗地里做了些不可描述的事情!由于没有同源策略的限制,它向www.maimaimai.com发起了请求!聪明的你一定想到上面的话“服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中”,这样一来,这个不法网站就相当于登录了你的账号,可以为所欲为了!如果这不是一个买买买账号,而是你的银行账号,那……

2.没有同源策略限制的Dom查询

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

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

3.跨域方式
现在自己的服务器下创建一个html我的在80端口,在用koa搭建后台。
同源策略限制下接口请求的正确打开方式
1.JSONP
前端代码:

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>

  </body>
  <script type="text/javascript">
    /**
     * JSONP请求工具
     * @param url 请求的地址
     * @param data 请求的参数
     * @returns {Promise<any>}
     */
    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:9999/api/jsonp',
      data: {
        // 传参
        msg: 'helloJsonp'
      }
    }).then(res => {
      console.log(res)
    })

  </script>
</html>

后台代码:

const Koa  =require('koa')
var app = new Koa()
app.use(async ctx => {
      // 前端传过来的参数
      const query = ctx.request.query
      // 设置一个cookies
      ctx.cookies.set('tokenId', '1')
      // query.cb是前后端约定的方法名字,其实就是后端返回一个直接执行的方法给前端,由于前端是用script标签发起的请求,所以返回了这个方法后相当于立马执行,并且把要返回的数据放在方法的参数里。
      ctx.body = `${query.cb}(${JSON.stringify({msg: query.msg})})`
});

app.listen(9999);

2.CORS
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)跨域资源共享 CORS 详解。看名字就知道这是处理跨域问题的标准做法。CORS有两种请求,简单请求和非简单请求。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

简单请求:
前台代码:
前端什么也不用干,就是正常发请求就可以,如果需要带cookie的话,前后端都要设置一下,下面那个非简单请求例子会看到。

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

后台代码:

      const query = ctx.request.query
      // *时cookie不会在http请求中带上
      ctx.set('Access-Control-Allow-Origin', '*')
      ctx.cookies.set('tokenId', '2')        //这里的setCookie无效
      ctx.body = {msg: query.msg}

非简单请求
前台代码:

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

后台代码:

const query = ctx.request.query
  // 如果需要http请求中带上cookie,需要前后端都设置credentials,且后端设置指定的origin
  ctx.set('Access-Control-Allow-Origin', 'http://localhost')
  ctx.set('Access-Control-Allow-Credentials', true)
  // 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)
  // 这种情况下除了设置origin,还需要设置Access-Control-Request-Method以及Access-Control-Request-Headers
  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 = {msg: query.msg}

3.代理
想一下,如果我们请求的时候还是用前端的域名,然后有个东西帮我们把这个请求转发到真正的后端域名上,不就避免跨域了吗?这时候,Nginx出场了。
Nginx配置

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

前台代码:

/ 请求的时候直接用回前端这边的域名http://localhost:80,这就不会跨域,然后Nginx监听到凡是localhost:80/api这个样子的,都转发到真正的服务端地址http://localhost:80 
fetch('http://localhost:/api/iframePost', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    msg: 'helloIframePost'
  })
})

Nginx转发的方式似乎很方便!但这种使用也是看场景的,如果后端接口是一个公共的API,比如一些公共服务获取天气什么的,前端调用的时候总不能让运维去配置一下Nginx,如果兼容性没问题(IE 10或者以上),CROS才是更通用的做法吧。

同源策略限制下Dom查询的正确打开方式
1.postMessage
window.postMessage() 是HTML5的一个接口,专注实现不同窗口不同页面的跨域通讯。
为了演示方便,我们将hosts改一下:127.0.0.1 crossDomain.com,现在访问域名crossDomain.com就等于访问127.0.0.1
这里是http://localhost:9099/#/crossDomain,发消息方

<template>
  <div>
    <button @click="postMessage">给http://crossDomain.com:9099发消息</button>
    <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: {
    // 向http://crossdomain.com:9099发消息
    postMessage () {
      const iframe = window.frames['crossDomainIframe']
      iframe.postMessage('我是[http://localhost:9099], 麻烦你查一下你那边有没有id为app的Dom', 'http://crossdomain.com:9099')
    }
  }
}
</script>

这里是http://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可以作为targetOrigin
        e.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟,这就是你想知道的结果:${document.getElementById('app') ? '有id为app的Dom' : '没有id为app的Dom'}`, e.origin);
      }
    })
  }
}
</script>

2.document.domain
这种方式只适合主域名相同,但子域名不同的iframe跨域。
比如主域名是http://crossdomain.com:9099,子域名是http://child.crossdomain.com:9099,这种情况下给两个页面指定一下document.domain即document.domain = crossdomain.com就可以访问各自的window对象了。

最后

本文引用:

https://segmentfault.com/a/1190000015597029#articleHeader6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值