老生常谈的前端高频面试题 ---- 浏览器的跨域问题

老生常谈的前端高频面试题 ---- 浏览器的跨域问题_第1张图片

浏览器的跨域问题

为了防止网站遭到恶意攻击,导致信息被窃取,所以浏览器设计了同源策略。

为什么浏览器会禁止跨域

浏览器是很开放的,只要在地址栏里面输入网址或者点击某个链接就可以访问了。正是因为这种开放的形态,才需要对浏览器做出限制,保护用户的信息安全。因此同源策略就是用来保护信息安全的。

什么是同源策略

同源政策由 Netscape 公司引入浏览器。同源策略是一个安全策略。所谓的同源,指的是协议,域名,端口相同。

浏览器出于安全方面的考虑,只允许本域名下的接口交互,不同源的客户端脚本,在没有明确授权的情况下,不能读写对方的资源。

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 JS 对象无法获取

如何解决跨域问题

JSONP

JSONP 实现跨域的原理

浏览器的同源策略限制不允许跨域请求;但页面中的 scriptimgiframe标签是例外不受同源策略限制。

Jsonp 就是利用script标签跨域特性进行请求,通过 ,从而触发对指定地址的GET请求

服务器端对这个GET请求进行处理,并返回字符串 "myCallback('response value')",前端script加载完之后,其实就是在script中执行 myCallback('response value'),就完成了跨域的请求,因此就是只能用GET。

JSONP 只能发 GET 请求,因为本质上 script 加载资源就是 GET

优缺点

优点:兼容性好,可以解决主流浏览器的跨域数据访问的问题。

缺点:只能进行 GET 请求,具有局限性,不安全。

<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
  function jsonp(data) {
    console.log(data)
  }
</script>
const jsonp = ({ url, params, callbackName }) => {
    const generateUrl = () => {
        let dataSrc = ''
        for (let key in params) {
            if (params.hasOwnProperty(key)) {
                dataSrc += `${key}=${params[key]}&`
            }
        }
        dataSrc += `callback=${callbackName}`
        return `${url}?${dataSrc}`
    }
    return new Promise((resolve, reject) => {
        const scriptEle = document.createElement('script')
        scriptEle.src = generateUrl()
        document.body.appendChild(scriptEle)
        window[callbackName] = data => {
            resolve(data)
            document.removeChild(scriptEle)
        }
    })
}

发送 POST 请求,使用 iframe

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 是 “跨域资源共享” (Cross-origin resource sharing ) 的缩写,跨域资源共享(CORS)是一种机制,是 W3C 标准。它允许浏览器向跨源服务器,发出XMLHttpRequestFetch请求。并且整个CORS通信过程都是浏览器自动完成的,不需要用户参与。解决 AJAX 只能同源通信的限制问题。

前提是,浏览器必须支持这个功能,并且服务端也必须同意这个跨域请求。

CORS通信与AJAX没有任何差别。只不过,浏览器会在请求中携带一些头信息,需要以此判断是否运行其跨域,然后在响应头中加入一些信息即可。

设计思想

使用自定义 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是成功还是失败。

两种请求方式
  • 简单请求: 浏览器直接发出 CORS 请求。具体来说,在头信息之中,增加一个Origin字段。

    (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

当浏览器发现发现的ajax请求是简单请求时,会在请求头中携带一个字段:Origin。Origin中会指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域。

如果服务器允许跨域,需要在返回的响应头中携带下面信息:

Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true

Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*,代表任意
Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true

注意:如果跨域请求要想操作cookie,需要满足3个条件:

  1. 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。
  2. 浏览器发起ajax需要指定withCredentials 为true
  3. 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名。
  • 高级请求: 必须首先使用 OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。服务器对 AJAX 跨域请求设置限制条件。不符合简单请求的条件,会被浏览器判定为特殊请求,,例如请求方式为PUT。

预检请求

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

非简单请求会发出一次预检测请求,返回码是204,预检测通过才会真正发出请求,这才返回200。这里通过前端发请求的时候增加一个额外的headers来触发非简单请求。

在这里插入图片描述

一个“预检”请求的样板:

OPTIONS /cors HTTP/1.1
Origin: http://manage.leyou.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.leyou.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

与简单请求相比,除了Origin以外,多了两个头:

Access-Control-Request-Method:接下来会用到的请求方式,比如PUT
Access-Control-Request-Headers:会额外用到的头信息

预检请求的响应

服务的收到预检请求,如果许可跨域,会发出响应:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
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

除了Access-Control-Allow-Origin和Access-Control-Allow-Credentials以外,这里又额外多出3个头:

Access-Control-Allow-Methods:允许访问的方式
Access-Control-Allow-Headers:允许携带的头
Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了
如果浏览器得到上述响应,则认定为可以跨域,后续就跟简单请求的处理是一样的了。

请求流程
  • 浏览器先根据同源策略对前端页面和后台交互地址做匹配,若同源,则直接发送数据请求;若不同源,则发送跨域请求。
  • 服务器收到浏览器跨域请求后,根据自身配置返回对应文件头。若未配置过任何允许跨域,则文件头里不包含 Access-Control-Allow-origin 字段,若配置过域名,则返回 Access-Control-Allow-origin + 对应配置规则里的域名的方式
  • 浏览器根据接受到的响应头里的 Access-Control-Allow-origin 字段做匹配,若无该字段,说明不允许跨域,从而抛出一个错误;若有该字段,则对字段内容和当前域名做比对,如果同源,则说明可以跨域,浏览器接受该响应;若不同源,则说明该域名不可跨域,浏览器不接受该响应,并抛出一个错误。

webpack 代理

通过 webpack 中的 proxy 进行代理,从而解决跨域的问题。

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://www.baidu.com/',
        pathRewrite: { '^/api': '' },
        changeOrigin: true, // target是域名的话,需要这个参数,
        secure: false, // 设置支持https协议的代理
      },
      '/api2': {
        .....
  	  }
    }
  }
};

webpack proxy ,就是 webpack 提供的解决跨域的方案。其基本行为是接受客户端发送的请求后转发给其他的服务器,目的是为了便于开发者在开发的模式下解决跨域的问题。要想实现代理必须要一个中间服务器, webpack 提供服务器的工具是 webpack-dev-server,只适用于开发阶段。

原理( http-proxy-middleware 中间件)

proxy 工作原理上市利用 http-proxy-middleware 这个 http 代理中间件,实现请求转发给其他的服务器。如下:在开发阶段,本地地址是 Http://loaclhost:3000 , 该浏览器发送一个前缀带有 /api 标识的向拂去器请求数据,但是这个服务器只是将这个请求转发给另一台服务器:

const express = require('express');
const proxy = require('http-proxy-middleware');

const app = express();

app.use('/api', proxy({target: 'http://www.example.org', changeOrigin: true}));
app.listen(3000);

// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar

在开发阶段,webpack-dev-server 会自动启动一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost 的一个端口上的,而后端服务器又是运行在另一个地址上。所以在开发阶段中,由于浏览器的同源策略,当本地访问的时候就会出现跨域资源请求的问题,通过设置 webpack proxy 实现代理请求后,相当于浏览器和服务器之间添加了一个代理商。当本地发送请求的时候,中间服务器会接受这个情求,并将这个请求转发给目标服务器,目标服务器返回数据后,中间服务器又会将数据返回给浏览器,当中间服务器将数据返回给服务器的时候,它们两者是同源的,并不会存在跨域的问题。服务器和服务器之间是不会存在跨域资源的问题的。

参考文章:
https://juejin.cn/post/6844903976505344013
https://www.jianshu.com/p/98d4bc7565b2
https://segmentfault.com/a/1190000015597029
https://segmentfault.com/q/1010000009708151

你可能感兴趣的:(HTTP,http,ajax,跨域,webpack,jsonp)