跨域的原理及解决方案


一、何为跨域

广义的跨域是指一个域下的文档和脚本试图请求另一个域下的资源。
🌰

  1. 资源跳转: A链接、重定向、表单提交
  2. 资源嵌入:<link>、<script>、<img>、<frame>等DOM标签,还有样式中background:url()、@font-face()等文件外链
  3. 脚本请求: Js发起的Ajax请求、DOM和Js对象的跨域操作等
    而我们常说的跨域,是指浏览器不能执行其他网站的脚本。它是由于浏览器的同源策略限制造成的。

同源策略: 浏览器对JS实施的安全限制,只要“协议、域名、端口号”有任何一个不同,都被当做是不同的域。缺少哦同源策略,浏览器很容易受到XSS、CSFR等攻击‼️

❌同源策略限制以下几种行为:

  • cookie、LocalStorage和IndexDB无法读取
  • DOMJS对象无法获得
  • AJAX请求不能发送

关于XSS和CSRF, cc之后会专门整理关于网络安全防范的文章~

二、跨域的原理

跨域的原理就是通过各种方式,避开浏览器的安全限制。

三、跨域的解决方案

我们可以通过多种方式实现跨域,常见的如下七种,大致分为iframeapi跨域:

  1. JSONP(JSON with Padding): 开发人员创造的一种非官方跨域数据交互协议。Ajax请求受到同源策略的影响,不允许进行跨域请求,而script标签src属性中的链接却可以访问跨域的js脚本,web页面调用JS文件不受浏览器同源策略的限制。利用这个特性,服务端不再返回json格式的数据,而是返回一段调用某个函数的js代码,在src中进行调用,这样就可以实现跨域。 jsonp通过动态创建script,再请求一个带参网址实现跨域通信,解决老版本浏览器跨域数据访问问题,流程如下:

    1️⃣. 首先前端设置好回调参数,并将其作为URL的参数
    2️⃣. 服务器端收到请求之后,通过该参数获取到回调函数名,并将数据放在参数中返回
    3️⃣. 收到结果后因为是script标签,所以浏览器当做脚本运行。

详细说一下前端实现JSONP:

  • 原生实现
var script = document.createElement('script')
script.type = 'text/javascript'

// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback'
document.head.appendChild(script)

// 回调执行函数
function handleCallback(res){
  alert(JSON.stringify(res))
}

服务端返回如下(返回时执行全局函数):

handleCallback({"status": true, "user": "admin"})
  • Jquery / Ajax
$.ajax({
  url: 'http://www.domain2.com:8080/login',
  type: 'get',
  dataType: 'jsonp', // 格式方法为jsonp
  jsonpCallback: "handleCallback", // 自定义回调函数名
  data: {}
});
  • Vue.js
this.$http.jsonp('http://ww.domain2.com:8080/login',{
  params: {},
  jsonp: 'handleCallback'
}).then((res) =>{
  console.log(res)
})

后端Node.js代码示例:

var querystring = require('querystring')
var http = require('http')
var server = http.createServer()

server.on('request', function(req, res){
  var params = qs.parse(req.url.split('?')[1])
  var fn = params.callback 
   
  // jsonp返回设置 
  res.writeHead(200, {'Content-Type': 'text/javascript'})
  res.write(fn + '(' + JSON.stringify(params) + ')')

  res.end()
});
server.listen('8080')
console.log('Server is running at port 8080 ...')

常见的手写jsonp算法示例:

jsonp原理:
借助script标签不受同源策略限制的特点,把script标签的src指向请求的服务端地址。

function jsonp(url, data = {}, callback = 'callback') {
   // 处理json对象,将参数拼接为url
   data.callback = callback
   let params = []
   for(let key in data) {
     params.push(key + '=' + data[key])
   }
   let script = document.createElement('script')
   script.src = url + '?' + params.join('&')
   document.body.appendChild(script)

   return new Promise((reslove, reject) => {
     window[callback] = (data) => {
       try {
         reslove(data)
       } catch (e) {
         reject(e)
       } finally {
         // 移除script元素
         script.parentNode.removeChild(script)
       }
     }
   })
}

// 使用jsonp请求数据
jsonp('http://dsbcjkxs.index', {
  page: 1,
  cate: 'recommend',
}, 'jsonpcallback').then(data => {
  console.log(data)
})
  1. CORS跨域资源共享(Cross-origin resource sharing):是一个W3C标准,允许浏览器向跨源服务器发送XMLHTTP Request请求,从而克服了ajax只能同源使用的限制,实现CORS的关键是服务器,只要服务器实现了CORS接口,就可以进行跨源通信。

    1. 前端的逻辑十分简单,正常发起ajax即可,成功的关键在于服务器Access-Control-Allow-Origin是否包含请求页面的域名,若要携带cookie请求,前后端都需要设置。如果不包含的话,浏览器将认为这是一次失败的异步请求,将会调用xhr.onerror中的函数。整个Cors的通信过程都是浏览器自动完成,不需要用户参与。

    2. CORS使用简单,支持POST方式,但是存在兼容问题

    3. 浏览器将CORS请求分为两类:简单请求非简单请求,浏览器对于这两种的处理不同

    4. 简单请求(simple request)需满足两大条件:
      1) 请求方法是三种之一:HEAD、GET、POST
      2) HTTP的头部信息不超过以下几个字段:
      🔻 Accept
      🔻 Accept-Language
      🔻 Content-Language
      🔻 Last-Event-ID
      🔻 Content-Type: 只限于三个值application/x-www-form-urlencodedmultipart/form-datatext-plain

    5. 非简单请求(not-so-simple request):
      1)对浏览器有特殊要求的请求,PUT、DELETE或者字段为Content-Type: application/json类型的请求
      2)在正式通信之前,会增加一次HTTP查询请求,成为**“预检”**请求(preflight),请求方法是OPTIONS
      🔻 Origin: 表示请求来自哪个源
      🔻Access-Control-Request-Method: 必选,列出CORS请求会用到哪些HTTP方法
      🔻 Access-Control-Request-Headers: 逗号分隔,指定浏览器CORS请求会额外发送的头信息字段

    6. 如果服务器同意本次请求,返回的响应会多出几个头信息字段:

      🔻 Access-Control-Allow-Origin: 返回Origin的字段或*
      🔻 Access-Control-Allow-Credentials: 可选, bool值,表示是否发送cookie
      🔻 Access-Control-Expose-Headers

    7. 🌰Vue框架中的设置:
      🔻 axios设置: axios.defaults.withCredentials = true
      🔻 vue-resource设置: Vue.http.options.credentials = true

      ➡️ 阮一峰——《跨域资源共享CORS详解》

  2. 代理跨域:把跨域的请求操作发送给后端,让后端帮忙代理请求,包括nginx代理跨域、nodejs中间件代理跨域、webSocket协议跨域

  3. document.domain + iframe跨域:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。【此方案仅限主域相同,子域不同的跨域应用场景】

  4. location.hash + iframe跨域 :a若要与b跨域相互通信,通过中间页c来实现,c与a同域,可以通过parent.parent来访问a页面的所有对象。三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

  5. window.name + iframe跨域 :通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域,巧妙地绕过了浏览器的跨域访问限制,但是又是安全操作。window.name属性的独特之处在于,name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的name值(2MB)

  6. postMessage跨域:它是HTML5 XMLHttpRequest Level 2中的API,可以跨域操作的window属性之一,主要解决:
    1)页面和其打开的新窗口的数据传递
    2)多窗口之间的消息传递
    3)页面与嵌套的iframe消息传递
    4)上述三个场景的跨域数据传递

JSONP和CORS的对比🍻

  • 使用目的相同,但是CORS比JSONP强大
  • JSONP只支持GET请求,CORS支持所有类型的HTTP请求
  • JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据

在众多跨域的解决方案中,最常见的还是前三种解决方案😆


Author: Casey Lu
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Casey Lu !
评论
 Previous
Javascript笔试刷题小结(三) Javascript笔试刷题小结(三)
生命不息,刷题不止,hhhhhhhhhh … 可爱的我又来了 1. Leetcode 1013. 将数组分成和相等的三个部分 **思路一:**最开始看到题目的时候cc是这么想的,既然能分成三个部分,不妨先计算每个部分的和,然后只要能成功地
2020-03-19
Next 
WebSocket的原理、实现和应用 WebSocket的原理、实现和应用
webSocket是一项可以让服务器将数据主动推送给客户端的技术,无论前后端都会接触。在实际的项目开发中,webSocket在用于双向传输和推送消息方面能做到灵活、简便、高效,发挥着不可或缺的作用。 一、什么是webSocket webSo
2020-03-15
  TOC