生存指南之 CORS + API Gateway

本文介绍了跨域资源共享的基本知识,以及如何避免云函数上 Serverless web API 的问题

构建 Web API 是 Serverless 应用中最流行的用例之一,您能在不增加其他操作开销的情况下,获得简单、可扩展的后端优势。

然而,如果您的网页正在调用后端 API,那么必须处理 跨域资源共享 (CORS) 的问题 ,如果您的网页向与您当前所在域的不同域发出 HTTP 请求,则它必须是 CORS 友好的。

如果您发现以下错误:

No 'Access-Control-Allow-Origin' header is present on the requested resource

那么本文可能对您有所帮助。

接下来,我们将介绍 Serverless + CORS 的相关信息,目录如下:

TL;DR

快速开始 🔜 如果您想快速解决 Serverless 应用中的 CORS,可以执行以下操作:

  1. 要处理 preflight requests,在每个 HTTP 端点中添加 enableCORS: trueintegratedResponse: true 标记:
# serverless.yml
service: products-service

provider:
  name: tencent
  region: ap-guangzhou
  runtime: Nodejs8.9 # Nodejs8.9 or Nodejs6.10

plugins:
  - serverless-tencent-scf

functions:
  getProduct:
    handler: handler.getProduct
    events:
      - apigw:
          name: api
          parameters:
            path: /product
            stageName: release
            # 修改成你的 API 服务 ID
            serviceId: service-xxx
            httpMethod: GET
            # 开启集成相应,这里必须开启,才能自定义响应 headers
            integratedResponse: true,
            # 开启 CORS
            enableCORS: true
  createProduct:
    handler: handler.createProduct
    events:
      - apigw:
          name: api
          parameters:
            path: /product
            stageName: release
            # 修改成你的 API 服务 ID
            serviceId: service-xxx
            httpMethod: POST
            # 开启集成相应,这里必须开启,才能自定义响应 headers
            integratedResponse: true,
            # 开启 CORS
            enableCORS: false
  1. 要处理 CORS headers,请在响应中返回 CORS headers。主要标头是 Access-Control-Allow-OriginAccess-Control-Allow-Credentials。示例如下:
'use strict';

// mock function
function retrieveProduct(event) {
  return {
    id: 1,
    name: 'good1',
    price: 10,
  };
}

// mock function
function createProduct(event) {
  const { queryString } = event;
  return {
    id: Number(queryString.id),
    name: 'good1',
    price: 10,
  };
}

module.exports.getProduct = (event, context, callback) => {
  const product = retrieveProduct(event);

  const response = {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Credentials': true,
    },
    body: JSON.stringify({
      product: product,
    }),
  };

  callback(null, response);
};

module.exports.createProduct = (event, context, callback) => {
  // Do work to create Product
  const product = createProduct(event);

  const response = {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Credentials': true,
    },
    body: JSON.stringify({
      product: product,
    }),
  };

  callback(null, response);
};

CORS preflight requests

如果您不是在进行「simple request」,那么浏览器会用 OPTIONS 方法,发送 preflight
request
到资源里,您请求的资源将使用 安全发送到资源 的方法返回,并且可以选择返回有效发送的标头。

我们拆开来看看:

浏览器什么时候发送 preflight requests?

您的浏览器会对几乎所有的跨域请求发送一个 preflight requests。(例外是「simple requests」,但这只是请求的一小部分)。大体上看,一个简单请求只是一个 GET request 或者 POST request,如果您不在此范围内,则需要进行预检。

对 preflight requests 的响应是什么?

对一个 preflight requests 的 响应包括其允许访问的资源,它允许在该资源的方法,如 GET, POST,
PUT 等。还可以包括被允许在该资源标头,如 Authentication

如何处理 Serverless 中的 preflight requests?

要设置 preflight requests,您只需要在 API Gateway 的端点上配置一个 OPTIONS 。幸运的是,你可以非常简单地使用 Serverless Framework 来完成。

只需要在 serverless.yml 添加设置 enableCORS: true

# serverless.yml

service: products-service

provider:
  name: tencent
  region: ap-guangzhou
  runtime: Nodejs8.9 # Nodejs8.9 or Nodejs6.10

plugins:
  - serverless-tencent-scf

functions:
  getProduct:
    handler: handler.getProduct
    events:
      - apigw:
          name: api
          parameters:
            path: /product
            stageName: release
            serviceId: service-lanyfiga
            httpMethod: GET
            # 开启集成相应,这里必须开启,才能自定义响应 headers
            integratedResponse: true,
            # 开启 CORS
            enableCORS: true
  createProduct:
    handler: handler.createProduct
    events:
      - apigw:
          name: api
          parameters:
            path: /product
            stageName: release
            serviceId: service-lanyfiga
            httpMethod: POST
            # 开启集成相应,这里必须开启,才能自定义响应 headers
            integratedResponse: true,
            # 开启 CORS
            enableCORS: false

CORS Response Headers

尽管 preflight request 仅适用于某些跨域请求,但每个跨域请求中都必须存在 CORS Response Headers,这意味着您必须将 Access-Control-Allow-Origin 添加进 handlers 的响应中。

如果您使用 cookies,还需要添加 Access-Control-Allow-Credentials

要与上面的 serverless.yml 匹配,handler.js 文件应该如下设置:

// handler.js

'use strict';

module.exports.getProduct = (event, context, callback) => {
  // Do work to retrieve Product
  const product = retrieveProduct(event);

  const response = {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Credentials': true,
    },
    body: JSON.stringify({
      product: product,
    }),
  };

  callback(null, response);
};

module.exports.createProduct = (event, context, callback) => {
  // Do work to create Product
  const product = createProduct(event);

  const response = {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Credentials': true,
    },
    body: JSON.stringify({
      product: product,
    }),
  };

  callback(null, response);
};

这里需要注意 responseheaders 属性,其中包含 Access-Control-Allow-OriginAccess-Control-Allow-Credentials

下面是一个简单的示例:

// hello.js

const middy = require('middy');
const { cors } = require('middy/middlewares');

// This is your common handler, no way different than what you are used to do every day
const hello = (event, context, callback) => {
  const response = {
    statusCode: 200,
    body: 'Hello, world!',
  };

  return callback(null, response);
};

// Let's "middyfy" our handler, then we will be able to attach middlewares to it
const handler = middy(hello).use(cors()); // Adds CORS headers to responses

module.exports = { handler };

CORS with Cookie credentials

在上面的示例中,我们给定了 "*" 作为 Access-Control-Allow-Origin 的值。但是,如果您使用 request using credentials 则不被允许。为了使浏览器能够响应,Access-Control-Allow-Origin 需要包含发出请求的特定来源。有两种方法可以解决。

首先,如果只有一个发出请求的原始网站,则可以将其硬编码到云函数的响应中:

// handler.js

'use strict';

module.exports.getProduct = (event, context, callback) => {
  // Do work to retrieve Product
  const product = retrieveProduct(event);

  const response = {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': 'https://myorigin.com', // <-- Add your specific origin here
      'Access-Control-Allow-Credentials': true,
    },
    body: JSON.stringify({
      product: product,
    }),
  };

  callback(null, response);
};

如果有多个原始网站使用您的 API,那么需要采用一种更加动态的方法。你可以检查 origin header 看看是否在被批准的来源列表中,如果是,则在 Access-Control-Allow-Origin 返回原点值。

// handler.js

'use strict';

const ALLOWED_ORIGINS = [
    'https://myfirstorigin.com',
    'https://mysecondorigin.com'
];

module.exports.getProduct = (event, context, callback) => {

  const origin = event.headers.origin;
  let headers;

  if (ALLOWED_ORIGINS.includes(origin) {
    headers = {
      'Access-Control-Allow-Origin': origin,
      'Access-Control-Allow-Credentials': true,
    },
  } else {
    headers = {
      'Access-Control-Allow-Origin': '*',
    },
  }

  // Do work to retrieve Product
  const product = retrieveProduct(event);

  const response = {
    isBase64Encoded: false,
    statusCode: 200,
    headers
    body: JSON.stringify({
      product: product
    }),
  };

  callback(null, response);
};

在这个示例中,我们检查 origin header 是否匹配。如果匹配,我们会在 Access-Control-Allow-Origin 包含特定来源,并声明 Access-Control-Allow-Credentials 允许的来源。如果 origin 不是我们允许的来源之一,则我们将包含标准 headers,如果来源尝试进行凭据请求,则将被拒绝。

小结

处理 CORS 确实是一件麻烦的事情,但是使用 Serverless Framework 会让处理步骤变得简单得多!而这也就意味着再也不会出现 No 'Access-Control-Allow-Origin' header is present on the requested resource 这样的错误啦!👋


传送门:

欢迎访问:Serverless 中文网,您可以在 最佳实践 里体验更多关于 Serverless 应用的开发!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,442评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,604评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,576评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,652评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,495评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,370评论 1 274
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,792评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,435评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,735评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,777评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,553评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,399评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,806评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,038评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,330评论 1 253
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,766评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,964评论 2 337

推荐阅读更多精彩内容