CORS 跨域

跨源资源共享(CORS)

CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。

因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax请求会失败。

跨源资源共享 (CORS)(或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它origin(域,协议和端口),这样浏览器可以访问加载这些资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的"预检"请求。在预检中,浏览器发送的头中标示有HTTP方法和真实请求中会用到的头。

出于安全性,浏览器限制脚本内发起的跨源HTTP请求。 例如,XMLHttpRequest 和 Fetch API 遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

访问控制场景

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求.

  • 请求方法是以下三种方法之一: POST,GET,HEAD
  • HTTP的头部信息不超出以下几种字段 Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type 只限于三个值 application/x-www-form-urlencode,multipart/form-data, text/plain

凡是不同时满足上面两个条件,就属于非简单请求。

什么是预检请求

发送预检请求用来以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

image.png

触发预检请求

image.png

简单请求

简单请求不会触发预检请求 (OPTIONS), 浏览器直接发出CORS请求,会在头部信息中,增加一个 Origin字段。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

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

实现跨域的方案

JSONP

原理如下

  • 前端定义一个解析函数(如: jsonpCallback = function (res) {})
  • 通过params的形式包装script标签的请求参数,并且声明执行函数(如cb=jsonpCallback)
  • 后端获取到前端声明的执行函数(jsonpCallback),并以带上参数且调用执行函数的方式传递给前端
  • 前端在script标签返回资源的时候就会去执行jsonpCallback并通过回调函数的方式拿到数据了。

jsonp 的缺点是只能发送’get’请求, 优点就是 兼容性好。 但是也需要服务端的支持

function jsonp({url, params, cb}){
 return new Promise((resolve,reject)=>{
     const scritp = window.createElement('script')
     window[cb] = function (data){
         resolve(data)
         document.body.removeChild(script);
     }
     params = {...params, cb}
     const arrs = []
     for(let key in params){
         arrs.push(`${key}=${params[key]}`)
     }
     script.src = `${url}?{arrs.join('&')}`; 
      document.body.appendChild(script)
  })
}
function jsonp({url, params, cb}){
 return new Promise((resolve,reject)=>{
     const scritp = window.createElement('script')
     window[cb] = function (data){
         resolve(data)
         document.body.removeChild(script);
     }
     params = {...params, cb}
     const arrs = []
     for(let key in params){
         arrs.push(`${key}=${params[key]}`)
     }
     script.src = `${url}?{arrs.join('&')}`; 
      document.body.appendChild(script)
  })
}

示例

服务代码

const express = require("express");
const app = express();
app.get("/who", function (req, res) {
  let { name, cb } = req.query;
  console.log("name", name);
  res.end(`${cb}('你好${name}')`);
});
app.listen(3000);
const express = require("express");
const app = express();
app.get("/who", function (req, res) {
  let { name, cb } = req.query;
  console.log("name", name);
  res.end(`${cb}('你好${name}')`);
});
app.listen(3000);

客户端

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      //  只能发送 get请求
      function jsonp({ url, params, cb }) {
        return new Promise((resolve, reject) => {
          let script = document.createElement("script");
          window[cb] = function (data) {
            resolve(data);
            document.body.removeChild(script);
          };
          params = { ...params, cb };
          let arrs = [];
          for (let key in params) {
            arrs.push(`${key}=${params[key]}`);
          }
          script.src = `${url}?${arrs.join("&")}`;
          document.body.appendChild(script);
        });
      }
      function show(data) {
        console.log("data", data);
      }

      jsonp({
        url: "http://localhost:3000/who",
        params: { name: "vincent" },
        cb: "show",
      }).then((res) => {
        console.log("res", res);
      });
    </script>
  </body>
</html>

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      //  只能发送 get请求
      function jsonp({ url, params, cb }) {
        return new Promise((resolve, reject) => {
          let script = document.createElement("script");
          window[cb] = function (data) {
            resolve(data);
            document.body.removeChild(script);
          };
          params = { ...params, cb };
          let arrs = [];
          for (let key in params) {
            arrs.push(`${key}=${params[key]}`);
          }
          script.src = `${url}?${arrs.join("&")}`;
          document.body.appendChild(script);
        });
      }
      function show(data) {
        console.log("data", data);
      }

      jsonp({
        url: "http://localhost:3000/who",
        params: { name: "vincent" },
        cb: "show",
      }).then((res) => {
        console.log("res", res);
      });
    </script>
  </body>
</html>

window.name

postMessage

MDN·CORS

阮一峰 CORD