用 nginx 代理 MailChimp API 并支持跨域

tl;dr

nginx 既灵活方便,又功能丰富,可以帮助我们实现添加跨域头、URL 重写以及隐藏敏感信息等功能。本文介绍在使用 MailChimp
进行邮件营销中遇到的一个普遍问题,分析了浏览器发起跨域请求时,在浏览器和服务器之间的交互过程,在此基础上提供了一个使用 nginx
反向代理实现的、灵活经济的解决方案。


MailChimp是最大的邮件营销解决方案提供商,官方网站 https://mailchimp.com。

通过 MailChimp 可以定制邮件模板、创建自己的邮件列表、发起各种营销活动,并支持统计有多少人打开了邮件以及其他一些 SNS 功能,客户可以随时订阅或取消订阅邮件。

MailChimp 提供了多种订阅表单,如普通表单、嵌入表单、弹窗表单以及由第三方集成的表单……用于收集用户邮箱及其他一些个人信息,然而这些表单都存在一些问题,如普通表单必须进行 captcha 校验,嵌入表单及弹窗表单不支持注册后自动跳转到自己的页面,并且所有这些表单还需要用户在收件箱里收取确认邮件并确认订阅,对于一家迫切需要更多订阅用户实现邮件营销的公司来说,为了尽可能多地获取订阅用户,这些校验和确认能省则省。而第三方集成的表单则需要按月收费,每月十几到上百美元不等,好钢要用在刀刃上,这也是一笔能省则省的开支。


MailChimp 还提供了 API 接口,其中 与列表成员相关的 API 接口 包含列表成员的创建、读取、编辑以及删除功能,因此,可以通过 AJAX 方式进行邮件订阅,订阅流程中是否加入 captcha 校验完全自主,用户订阅之后也不需要进行邮箱确认,并且完全免费。

但这些 API 也存在两个问题,一是不支持跨域,二是 MailChimp 提供的 API key 是全权限的,意味着一旦泄露,捣蛋分子就可以利用 MailChimp 的 API 接口对你的 MailChimp 账号为所欲为。

因此,我们如果希望使用 MailChimp 的 API 实现用户订阅,就需要解决上述两个问题,兼具灵活性和功能性的 nginx 是个不错的选择。


首先我们需要一个 nginx,在本地试验的话用 docker 就可以了,在命令行终端执行 docker pull nginx,稍等片刻就可把 nginx 最新版的 docker 镜像拉取下来了。如果网络状况不佳,还可以配置 daocloud 的 Docker 加速器。

打开终端窗口运行 docker run --name temp-nginx nginx,再打开一个终端窗口,然后使用 docker cp temp-nginx:/etc/nginx/nginx.conf. 和 docker cp temp-nginx:/etc/nginx/conf.d/default.conf. 这两个命令从容器中将 nginx 相关的配置拷贝出来,拷贝出来的配置文件主要作为我们自己编写配置文件的参考,即使之前没有配置过 nginx,也可以依葫芦画瓢完成基本的配置。

nginx.conf 文件中通过 include 指令包含了 default.conf 文件的内容,我们首先把两个文件简化合并成一个文件,并删除不必要的注释,命名为 mailChimp.conf。

user nginx;
worker_processes 1;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
  }
}

目的达到,我们可以“卸磨杀驴”,随后运行 docker stop temp-nginx && docker rm temp-nginx 将前面创建的容器停止并删除。


为了方便调试,我们同时编写一个 shell 脚本,用于随时使用创建的 mailChimp.conf 配置文件启动 docker 容器,并将 localhost 的 8080 端口绑定到容器的 80 端口上,这样我们就可以向 http://localhost:8080/ 发起请求,并在命令行终端查看日志,一旦配置出现问题可以随时使用 ctrl+c 停止 docker 容器,修改配置后重新运行该脚本即可。

#!/usr/bin/env bash

docker rm mailChimp-nginx

docker run --name mailChimp-nginx -v /path/to/mailChimp.conf:/etc/nginx/nginx.conf:ro -it -p 8080:80 nginx

接下来我们开始编写自己的配置文件,实现指向 MailChimp 的代理,并在请求中添加 API key 进行鉴权,同时在响应中添加跨域头,以满足跨域需求。

user nginx;
worker_processes 1;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    location /maillist/subscribe {
      if ($request_method = GET) {
        return 403;
      }

      add_header Access-Control-Allow-Origin * always;
      add_header Access-Control-Allow-Methods PUT,OPTIONS;
      add_header Access-Control-Allow-Headers Content-Type;

      if ($request_method = OPTIONS) {
        return 204;
      }

      proxy_set_header Authorization 'apikey {apiKey}';
      rewrite /maillist/subscribe/(.*)$ /3.0/lists/{listId}/members/$1 break;
      proxy_pass https://us{siteNumber}.api.mailchimp.com;
    }
  }
}

在上面的配置文件中,{apiKey} 用我们从 MailChimp 创建的 API key 代替,{listId} 用需要实现订阅的邮件列表的 ID 代替,而 {siteNumber} 则是 API key 的后缀数字。


我们仍然使用 docker 镜像中 /usr/share/nginx/html 目录的内容作为网站首页,同时添加一个 /maillist/subscribe 路径。

由于 MailChimp 的 API key 是全权限的,为避免用户直接通过浏览器访问 /maillist/subscribe/ 获取 MailChimp 的 /3.0/lists/{listId}/members/ 的内容,首先需要对 HTTP Method 为 GET 的请求返回 403 Forbidden,在配置文件中通过 if 指令进行 Method 判断,并通过 return 指令返回 403 代码。

基于我们浏览器进行跨域请求的了解,浏览器在正式发起跨域请求之前,先要发起一个 Method 为 OPTIONS 的请求(Preflighted Requests,预检请求),通过该请求获取服务端的响应以了解服务端是否支持跨域、支持哪些 Method、以及支持哪些 Header 等信息,简言之,获取服务器支持的 HTTP 请求特性,这些响应头在 OPTIONS 请求的响应以及正式请求响应中必须一致。

因此我们接着进行 HTTP 响应头的设置,由于后面我们对请求及响应进行了代理转发,而转发的 MailChimp 响应中不包含 Access-Control-Allow-Origin 跨域响应头,因此这里使用 add_header 添加 Access-Control-Allow-Origin 时,需要在后面加上 always,表示始终加上 Access-Control-Allow-Origin。

此外还需要添加 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers 响应头。

经试验,如果采用 MailChimp 列表成员添加的 API,使用 POST Method,实现订阅邮件的添加,则当用户通过其他途径退订了列表之后,无法重新订阅,因此改为采用 MailChimp 列表成员编辑的 API,这是一个 PUT Method,请求 URL 路径中包含用户邮箱地址的 MD5 值,当请求的用户在列表中不存在时为添加,当用户存在时则为修改。因此这里的 Access-Control-Allow-Methods 设置为 PUT,OPTIONS

MailChimp API 仅支持 JSON 格式的数据,这需要在请求中设置 Content-Type 为 application/json; charset=utf-8,同时还需要在 nginx 的响应中设置 Access-Control-Allow-Headers 为 Content-Type

设置完毕,对于 Method 为 OPTIONS 的请求,我们不需要返回 body,因此再次使用 if 指令进行 Method 判断,并返回 204 代码表示无正文。

最后,就是代理转发的设置了,首先使用 proxy_set_header 指令设置鉴权请求头,然后使用 rewrite 指令进行 URL 重写,使用正则表达式获取 /maillist/subscribe/ 之后的 E-Mail 的 MD5,拼接成 MailChimp API 请求的 URL,最后,通过 proxy_pass 指令实现代理转发。


使用 Fetch 从前端发起请求,相关代码如下,API key、list ID 等敏感信息已被隐藏:

var headers = new Headers()
headers.append('Content-Type', 'application/json; charset=utf-8')

var body = '{"email_address":"[email protected]","status":"subscribed"}'

fetch('http://localhost:8080/maillist/subscribe/f1c49ab3e7dd54b1daee1f4bdc0a1f78', {
  method: 'PUT',
  headers,
  body
}).then(res => res.json()).then(json => {
  console.log(json)
}).catch(err => {
  console.log(err)
})

从控制台上看到结果如下:


实际项目中,body 使用 JSON.stringify 进行序列化,请求路径中的参数还需要使用 MD5 库进行 MD5 值计算。

最终的配置可以在 nginx 的 docker 镜像基础上创建一个包含了上述配置的 docker 镜像,然后部署到 DaoCloud 或其他提供 docker 容器服务的站点上去。下面是一个部署在 DaoCloud 的代理,http://mario-studio.daoapp.io/,使用 Vue 创建了一个超级简单的 订阅表单,欢迎大家订阅我的邮件列表,虽然还不知道我会发些什么(手动比心

你可能感兴趣的:(daocloud,docker,mailchimp,cors,nginx)