美文网首页
CORS — 详解 & 实战

CORS — 详解 & 实战

作者: LK2917 | 来源:发表于2019-12-25 16:03 被阅读0次

WEB开发们都知道,出于安全原因,浏览器有个同源策略,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。一个HTTP请求的URL的协议、域名、端口三者中的任何一个与当前源不同,则视为跨域请求。如果不做处理,我们看到chrome抛出一个错误:


cross-origin-request-error

而在实际的场景中,我们会有很多情况下需要进行跨域请求,所以跨域解决方案和其原理几乎是WEB开发必须要掌握的知识。下面列举几种常见的跨域方案:

跨域的几种解决方案

  • JSONP:这是跨域请求的一个经典方案,其主要原理是通过JS动态创建<script>标签获取指定资源,然后前后端约定一个callback来获取json数据,<script><iframe>这些具有src属性的标签都是可直接跨域获取资源的,这种方式其实只是巧妙地绕过跨域限制,而且有其局限性,比如很明显的,只能发送GET请求,而且要判断请求是否失败也比较棘手。
  • Proxy代理:由于同源策略只是浏览器的限制,服务器端并没有这个限制,所以只要A域客户端将请求发送一个代理服务器,然后由代理服务器去请求B域服务器就行了,比如前后端分离的工程,本地调试的时候我们启用nodejs代理服务、线上部署通过nginx代理转发等,都属于这个跨域模式。同样的,这个本质上也只是绕过浏览器的跨域限制而已。
  • CORS(Cross-Origin Resource Sharing):跨域资源共享标准,本文重点研究对象。

CORS初尝试

假设现在服务端有个获取股票列表的接口,并已设置允许跨域(后文将介绍如何设置),其中
客户端地址:http://localhost:3000
服务端地址:http://localhost:7001

页面上设置了个按钮用以获取股票列表:


获取股票列表的前端代码:

axios({
  url: 'http://localhost:7001/api/getStocks',
}).then((res) => {
  const data = res.data;
  this.setState((prevState) => ({
    list: prevState.list.concat(data.data),
  }));
});

此时发送的请求状态为:


可以看到请求直接成功并返回了数据,乍看之下除了Response Headers多了一些Access-Control-Allow-*字段外,和普通请求没什么区别。
过了段时间,出于安全角度考虑,现在要对这个接口进行token验证,,所以增加了一个请求头字段 access-token

axios({
  url: 'http://localhost:7001/api/getCounts',
  headers: {
    'access-token': 'abcdefg',
  },
}).then((res) => {
  const data = res.data;
  this.setState({
    count: data.data,
  });
});

这时再查看请求的发送情况,奇怪的事情出现了,现在浏览器竟然发出去了两个请求!查看之后,会发现第一个请求方法为OPTIONS,状态码为204,什么数据都没有返回!第二个请求才是我们真正想要的请求,GET请求,且状态码为200,将股票列表返回了:

第一个请求
第二个请求

所以第一个OPTIONS请求是什么?为什么会发送这个请求?

CORS工作原理

CORS新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求,浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。预检请求头中Access-Control-Request-Method字段告诉服务器实际请求的方法,Access-Control-Request-Headers字段告知服务器实际请求中需要携带的自定义参数。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

简单请求和非简单请求

一般把无需发送OPTIONS的请求叫做简单请求,把需要发送OPTIONS的请求称为非简单请求复杂请求

其中简单请求必须满足以下几个条件(不满足所有下面条件的即为非简单请求):

  1. 请求方式只限于 GET、 HEAD、POST;
  2. 除以下头部信息外,不能自定义其他请求头字段 :
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(需要注意额外的限制)
    • Last-Event-ID
  3. Content-Type 的值只限于以下三种:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

附带身份凭证的请求

CORS (还有Fetch )的一个有趣特性是,可以基于 HTTP cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨域 XMLHttpRequestFetch请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置某个特殊标志位,例如我们的代码 axios 中可以加入withCredentials字段表示跨域请求时需要携带凭证:

axios({
  url: 'http://localhost:7001/api/getStocks',
  withCredentials: true, // 设置携带凭证
}).then((res) => {
  const data = res.data;
  this.setState((prevState) => ({
    list: prevState.list.concat(data.data),
  }));
});

此时我们发送一个简单请求会发现一个奇怪的事情:



明明请求已经返回了数据,但是页面上并没有渲染出来,事实上此时Chrome浏览器已经在控制台出现了报错信息:


这是因为如果跨域请求想要附带身份凭证,必须在服务端设置Access-Control-Allow-Credentialstrue,否则浏览器将不会把响应内容返回给请求的发送者。
另外,对于附带身份凭证的请求,服务器不得设置Access-Control-Allow-Origin的值为*

CORS响应头字段

注:以下例子为NodeJs中Egg框架的设置方法(事实上,Egg框架中你会选择egg-cors插件进行跨域设置),不同语言和框架请参照各自的文档。

1. Access-Control-Allow-Origin

语法为:Access-Control-Allow-Origin: <origin> | *,其中origin参数的值指定了允许访问该资源的外域 URI,如果跨域请求中携带了cookie,则不能指定其值为*。如:

ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000');
2. Access-Control-Allow-Methods

语法为:Access-Control-Allow-Methods: <method>[, <method>]*,用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。如:

ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS, DELETE');
3. Access-Control-Allow-Headers

语法为:Access-Control-Allow-Headers: <field-name>[, <field-name>]*,用于预检请求的响应。其指明了实际请求中允许携带的首部字段。如:

ctx.set('Access-Control-Allow-Headers', 'Content-Type, access-token');
4. Access-Control-Allow-Credentials

指定了当浏览器的credentials设置为true时是否允许浏览器读取response的内容。当用在对preflight预检请求的响应中时,它指定了实际的请求是否可以使用credentials。请注意:简单GET请求不会被预检;如果对此类请求的响应中不包含该字段,这个响应将被忽略掉,并且浏览器也不会将相应内容返回给网页。
如:

ctx.set('Access-Control-Allow-Credentials', true);
5. Access-Control-Expose-Headers

在跨域访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果要访问其他头,则需要服务器设置本响应头。如:

ctx.set('Access-Control-Expose-Headers', 'access-token');
6. Access-Control-Max-Age

语法为:Access-Control-Max-Age: <delta-seconds>,指定了preflight请求的结果能够被缓存多久(单位:秒)。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。如:

ctx.set('Access-Control-Max-Age', 86400);  // 86400秒内,即24小时内都有效

CORS请求头字段

1. Origin

origin 参数的值为源站 URI。它不包含任何路径信息,只是服务器名称。

2. Access-Control-Request-Method

用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。

3. Access-Control-Request-Headers

用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。

源码(Egg框架)

  1. router:
'use strict';

module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
  router.get('/api/getStocks', controller.home.getStocks);
};
  1. controller:
'use strict';

const Controller = require('egg').Controller;

class HomeController extends Controller {
  async index() {
    this.ctx.body = 'hello world';
  }

  async getStocks() {
    const { ctx } = this;
    const stocks = [{
      name: '上证指数',
      code: '1A0001'
    }, {
      name: '万科A',
      code: '000002'
    }, {
      name: '滨江集团',
      code: '002244'
    }];
    ctx.body = {
      code: 0,
      message: 'success',
      data: stocks,
    };
  }
}

module.exports = HomeController;

  1. config/plugin
'use strict';

/** @type Egg.EggPlugin */
exports.validate = {
  enable: true,
  package: 'egg-validate',
};

exports.cors = {
  enable: true,
  package: 'egg-cors',
}
  1. config/config.default
'use strict';

/**
 * @param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo => {
  /**
   * built-in config
   * @type {Egg.EggAppConfig}
   **/
  const config = exports = {};
  config.keys = appInfo.name + '_1574314669249_9332';
  config.middleware = ['errorHandler'];
  
  config.cors = {
    origin: 'http://localhost:3000',
    allowMethods: 'GET, HEAD, PUT, POST, DELETE, PATCH, OPTIONS',
    allowHeaders: 'access-token',
    credentials: true,
  };

  config.security = {
    // 关闭csrf验证
    csrf: {
      enable: false,
    },
    // 白名单
    domainWhiteList: ['*']
  };

  const userConfig = {
    myAppName: 'cors',
  };

  return {
    ...config,
    ...userConfig,
  };
};

相关文章

  • CORS — 详解 & 实战

    WEB开发们都知道,出于安全原因,浏览器有个同源策略,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。...

  • 跨域问题详解分析

    参考文档 CORS详解 跨域资源共享 CORS 详解 js中几种实用的跨域方法原理详解 跨域的那些事儿 跨域与跨域...

  • 跨域资源共享 CORS 详解

    跨域资源共享 CORS 详解

  • 跨域资源共享 CORS 详解

    跨域资源共享 CORS 详解

  • CORS 跨域资源共享

    跨域资源共享 CORS 详解 ajax同源 协议相同 域名相同 端口相同 CORS cross-origin re...

  • CORS 详解

    什么是跨域HTTP请求 现代浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵...

  • CORS详解

    前言 CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing...

  • Flutter开发实战初级(一)ListView详解

    Flutter开发实战初级(一)ListView详解 Flutter开发实战初级(一)ListView详解 本篇博...

  • 同源策略和跨域问题

    浏览器同源政策及其规避方法 跨域资源共享 CORS 详解

  • 跨域

    参考资料 HTTP访问控制(CORS)跨域解决方案跨域详解

网友评论

      本文标题:CORS — 详解 & 实战

      本文链接:https://www.haomeiwen.com/subject/skdowctx.html