cors 学习笔记

422 阅读5分钟

cors 学习笔记

同源策略

  • 作用:用来控制不同源之间的交互。
  • 定义:如果两个 URL 的协议、端口和域名都相同的话,则这两个 URL 就是同源的。

不同源之间的交互:

  • 跨域写操作:一般是被允许的,如链接、重定向、表单提交。
  • 跨域资源嵌入:一般是被允许的,如img、script标签(JSONP实现跨域的前提条件)。
  • 跨域读操作:一般是不被允许的,读操作指的是从服务器获取资源(有response),即所有http接口请求都不被允许。

浏览器自己是可以发起跨域请求的(比如a标签、img标签、form表单等),但是Javascript是不能去跨域获取资源(如ajax)。

跨域问题的控制台报错截图:

640.webp

跨域资源共享

跨域资源共享(CORS)是一种基于 HTTP 首部的机制。

  • 该机制通过允许服务器标识除了它自己以外的其它 origin,这样浏览器可以访问加载这些资源。
  • 使用options方法发起预检请求来询问服务器是否允许要发送的真实请求。在预检请求中,浏览器发送的首部中标识有 HTTP 方法和真实请求中会用到的首部。

CORS 的几个使用场景:

  • XMLHttpRequest 和 Fetch 发起的跨源 HTTP 请求
  • css 中通过@font-face使用跨源字体资源
  • 使用drawImage将 Images/Videos 画面绘制到 Canvas

某些请求不会触发 CORS 预检请求,称这样的请求为简单请求。关于简单请求的限制条件,可看MDN 介绍

相关响应报文首部

Access-Control-Allow-Origin:该响应的资源是否被允许与给定的 origin 共享

Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: https://developer.mozilla.org

Access-Control-Allow-Methods:在对预检请求的应答中,明确了客户端所将访问的资源允许使用的方法。

Access-Control-Allow-Methods: POST,GET,OPTIONS

Access-Control-Expose-Headers:列出了哪些首部可以作为响应的一部分暴露给外部。默认情况下只有七种首部可以暴露给外部

Access-Control-Allow-Headers:在对预检请求的应答中,列出了可以在真实请求中出现的请求首部

Access-Control-Max-Age:表示预检请求的应答结果中的信息可以被缓存多久

Access-Control-Allow-Credentials:是否允许客户端携带验证信息,如 cookie。默认机制下,cors 不会携带 cookie 的。

  1. 服务器端的响应报文首部需要带Access-Control-Allow-Credentials: true
  2. 浏览器端发起 ajax 需要指定withCredentialstrue
  3. 响应报文首部中的Access-Control-Allow-Origin不能为*

相关请求报文首部

Origin:浏览器会将Origin请求首部添加到:所有的跨域请求、除 GET、HEAD 请求外的同源请求

preflight request 预检请求:请求方法为 options,当有需要的时候,浏览器会自动发出一个预检请求,不需要前端开发者自己去发。

preflight request 一般包括的请求首部有:

  • Origin
  • Access-Control-Reuqest-Method
  • Access-Control-Request-Headers

@koa/cors

使用方式:

const Koa = require("koa");
const cors = require("@koa/cors");

const app = new Koa();
app.use(cors());

主要源码的阅读:

// 省略一些配置项格式化、合并代码

return async function cors(ctx, next) {
  // 如果Origin请求首部不存在,则结束cors的配置,因为请求不符合规范
  const requestOrigin = ctx.get("Origin");

  // Always set Vary header
  // https://github.com/rs/cors/issues/10
  ctx.vary("Origin");

  // 跳出
  if (!requestOrigin) return await next();

  // origin的处理配置
  let origin;
  if (typeof options.origin === "function") {
    origin = options.origin(ctx);
    if (origin instanceof Promise) origin = await origin;
    if (!origin) return await next();
  } else {
    origin = options.origin || requestOrigin;
  }

  // credentials的处理配置
  let credentials;
  if (typeof options.credentials === "function") {
    credentials = options.credentials(ctx);
    if (credentials instanceof Promise) credentials = await credentials;
  } else {
    credentials = !!options.credentials;
  }

  const headersSet = {};

  function set(key, value) {
    ctx.set(key, value);
    headersSet[key] = value;
  }

  // 预检请求使用的HTTP方法是options
  if (ctx.method !== "OPTIONS") {
    // Simple Cross-Origin Request, Actual Request, and Redirects
    set("Access-Control-Allow-Origin", origin);

    if (credentials === true) {
      // 允许客户端携带验证信息
      set("Access-Control-Allow-Credentials", "true");
    }

    if (options.exposeHeaders) {
      // 预检请求才配置Access-Control-Expose-Headers
      set("Access-Control-Expose-Headers", options.exposeHeaders);
    }

    if (!options.keepHeadersOnError) {
      return await next();
    }
    try {
      return await next();
    } catch (err) {
      const errHeadersSet = err.headers || {};
      const varyWithOrigin = vary.append(
        errHeadersSet.vary || errHeadersSet.Vary || "",
        "Origin"
      );
      delete errHeadersSet.Vary;

      err.headers = {
        ...errHeadersSet,
        ...headersSet,
        ...{ vary: varyWithOrigin },
      };
      throw err;
    }
  } else {
    // Preflight Request 预检请求

    /**
     *如果解析不出Access-Control-Request-Method请求首部,
     *则不是预检请求,直接终止跳出
     */
    if (!ctx.get("Access-Control-Request-Method")) {
      // this not preflight request, ignore it
      return await next();
    }

    ctx.set("Access-Control-Allow-Origin", origin);

    if (credentials === true) {
      // 允许客户端携带验证信息
      ctx.set("Access-Control-Allow-Credentials", "true");
    }

    if (options.maxAge) {
      // 在指定的时间内,不再需要发送预检请求
      ctx.set("Access-Control-Max-Age", options.maxAge);
    }

    if (options.allowMethods) {
      ctx.set("Access-Control-Allow-Methods", options.allowMethods);
    }

    let allowHeaders = options.allowHeaders;
    if (!allowHeaders) {
      allowHeaders = ctx.get("Access-Control-Request-Headers");
    }
    if (allowHeaders) {
      ctx.set("Access-Control-Allow-Headers", allowHeaders);
    }

    // 204 No Content 响应报文中没有实体的主体部分
    ctx.status = 204;
  }
};

expressjs/cors

使用方式:

const express = require("express");
const cors = require("cors");

const app = express();
app.use(cors());

主要源码的阅读:

function cors(options, req, res, next) {
  var headers = [],
    method = req.method && req.method.toUpperCase && req.method.toUpperCase();

  if (method === "OPTIONS") {
    // preflight 预检请求
    headers.push(configureOrigin(options, req)); // 根据不同情况设置Access-Control-Allow-Origin、Vary: Origin
    headers.push(configureCredentials(options, req)); // 配置Access-Control-Allow-Credentials
    headers.push(configureMethods(options, req)); // 配置Access-Control-Allow-Methods
    headers.push(configureAllowedHeaders(options, req)); // 配置Access-Control-Allow-Headers
    headers.push(configureMaxAge(options, req)); // 配置Access-Control-Max-Age
    
    // 预检请求才配置Access-Control-Expose-Headers
    headers.push(configureExposedHeaders(options, req)); // 配置Access-Control-Expose-Headers
    applyHeaders(headers, res);

    if (options.preflightContinue) {
      next();
    } else {
      // Safari (and potentially other browsers) need content-length 0,
      //   for 204 or they just hang waiting for a body
      res.statusCode = options.optionsSuccessStatus;
      res.setHeader("Content-Length", "0");
      res.end();
    }
  } else {
    // actual response 真实请求
    headers.push(configureOrigin(options, req));
    headers.push(configureCredentials(options, req));
    headers.push(configureExposedHeaders(options, req));
    applyHeaders(headers, res);
    next();
  }
}

存在的疑问

对于Vary首部、Vary: Origin的用法和含义,我不理解,如果有知道的网友,可以在评论区写出来~

参考