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 创建了一个超级简单的 订阅表单,欢迎大家订阅我的邮件列表,虽然还不知道我会发些什么(手动比心