跨域 - CORS / JSONP基础使用

224 阅读4分钟

1. 同源策略

  • :由URL 的方案(协议),主机(域名)和端口定义。协议+域名+端口

  • 同源策略: 为浏览器所规定的,如果JS运行在源A中,便只能获取源A的数据,而不能获取源B的数据,即不允许跨域。

  • 同源的判断: 若两个URL的协议、域名、端口号完成一致,才能算为同源。

2. 跨域(cross-origin)

跨域 / 跨源 应该只是翻译上的不同。

  • 跨域: 当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。

3. CORS

  • CORS:Cross-Origin Resource Sharing 跨域资源共享

    • 当两个不同源的源相互访问数据的时候,需要在被访问的源的响应头里面提前声明可以访问的源,告知浏览器那些源是可以访问自己的。(即服务端来指定哪些主机可以从这个服务端加载资源)

    • 具体使用 ACAO

      设置响应头Access-Control-Allow-Origin属性。

      • 仅允许来自 foo.example 的访问

        Access-Control-Allow-Origin:foo.example

      • 允许任意外域访问

        Access-Control-Allow-Origin: *

    • 缺点:兼容性问题,此方法IE9不支持。

3.1 CORS预检

当请求同时满足以下条件时,浏览器会认为它是一个简单请求:

  1. 使用下列方法之一:
  • GET
  • HEAD
  • POST
  1. Content-Type 的值仅限于下列三者之一:
  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded
  1. 请求头仅包含安全的字段,常见的安全字段如下
  • Accept
  • Accept-Language
  • Conent-Language
  • Content-Type

不符合以上条件的请求则为非简单请求。

若为非简单请求,则会经历以下的请求流程:

  1. 浏览器发送预检请求,询问服务器是否允许
  2. 服务器允许
  3. 浏览器发送真实请求
  4. 服务器完成真实的响应

假设我们要送一个请求如下:

// 需要预检的请求
fetch('http://crossdomain.com/api/user', {
  method: 'POST', // post 请求
  headers: {
    // 设置请求头
    a: 1, // 多的字段
    b: 2, // 多的字段
    'content-type': 'application/json', // 简单请求里不允许的值
  },
  body: JSON.stringify({ age: 18 }), // 设置请求体
});

第一步:浏览器发送预检请求,询问服务器是否允许

OPTIONS /api/user HTTP/1.1
Host: crossdomain.com
...
Origin: http://my.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: a, b, content-type

预检请求的特点:

  • 没有请求体
  • 请求方式为 OPTIONS
  • Access-Control-Request-Method为后续请求所使用的方法
  • Access-Control-Request-Headers为后续的真实请求会改动的请求头

第二步:服务器允许

Access-Control-Allow-Origin: http://my.com 
Access-Control-Allow-Methods: POST 
Access-Control-Allow-Headers: a, b, content-type 
Access-Control-Max-Age: 86400

响应无响应体,响应头中会有几个字段:

  • Access-Control-Allow-Origin:和简单请求一样,表示允许的源
  • Access-Control-Allow-Methods:表示允许的后续真实的请求方法
  • Access-Control-Allow-Headers:表示允许改动的请求头
  • Access-Control-Max-Age:告诉浏览器,多少秒内,对于同样的请求源、方法、头,都不需要再发送预检请求了

第三步:浏览器发送真实请求

第四步:服务器完成真实的响应

4. JSONP

  • JSONP:JSON with Padding

    • 使用场景

      在跨域的时候由于当前浏览器不支持CORS或者其他某些原因不支持CORS,必须使用另外一种方法跨域。于是我们就利用script标签的src请求一个js文件 ,这个js文件会执行一个回调,这个回调里面就有我们的数据。回调名字可以随机形成,以callback参数传递后台,后台以函数执行的形式返回。

    • 优点:兼容ie;可以跨域;

    • 缺点:由于是使用script标签,所以无法读取到同ajax一样那么精确的状态(状态码、响应头),只能了解请求成功与失败;只能发送get请求,不支持post;

  • 封装JSONP

    以下代码运行在客户端上

    function jsonp(url){
        return new Promise((resolve, reject)=>{ //使用Promise封装
            const callback = 'callbackName' + Math.random() //生成随机的回调函数名,以免重复
            window[callback] = data =>{ //该函数挂在window对象上
                resolve(data)
            }
            const script = document.createElement('script') //创建script元素
            script.src = `${url}?callback=${callback}` //src属性,所要请求的脚本,并以查询参数的方式传递回调函数名
            script.onload = ()=>{
                script.remove() //请求成功后移除标签
            }
            script.onerror = ()=>{
                reject() //失败则执行reject
            }
            document.body.appendChild(script) //将script标签插入文档中
        })
    }
    
    jsonp('http://xxx.com/yy.js').then(res=>console.log(res),err=>console.log(err))
    

    服务端接收到请求后返回的脚本内容为:

    window[callback](data)
    

    此处的callback为客户端请求的查询参数所提供,data则为服务端提供给客户端的数据。

    小结:

    1. 大致流程为

      • 客户端提供一个回调函数名
      • 客户端动态添加script元素发起跨域请求,并将回调函数名作为查询参数提供
      • 服务端返回一串js代码,该代码调用客户端的回调函数,该回调函数接收的实参为所想要传输的数据。
      • 客户端跨域请求完毕,回调函数被调用,对数据进行处理。
    2. 客户端做足了准备,需要服务端来触发调用这一切,当然最重要的还是所返回的数据。