qfdk

qfdk

喜欢碎碎念的小 🐭, 对开源情有独钟, 会说 🇫🇷, 喜欢折腾 “黑科技”, 徒步是日常
github

SaaS 平台捆绑多域名的解决方案

0x1 简介#

起因看到 jeff 的一个帖子 需求:给网站绑一万个域名,自动生成 HTTPS 证书 勾起来自己的某些回忆,我分享下我的方案吧.
这个方案 2019 年已经被投入生产环境并稳定运行。不过,遗憾的是现在这个项目因为某些原因我决定停了,有兴趣的小伙伴可以私聊下。开发了一个外卖平台,可以支持多个用户, 每个用户拥有独立后台,都可以拥有自己独立的域名。比如,如果我有一家叫做 AABBCC 的商店,平台会为我分配一个二级域名,比如 AABBCC.example.com。当然,这个商店也可以有自己的域名,比如AABBCC.com,而这两个域名实际上指向同一个站点。

0x2 简单思考#

为了实现以上方案,首先要考虑如何实现 SaaS 的多用户,因为每个用户是一个独立的个体,数据也需要分开储存。如何选择数据库呢?比如 MySQL 每个用户为一个表?或者是 mongodb 每个用户建立一个库?这里我选择了后者,原因如下,虽然是 SaaS 系统,但是每个用户有自己独立的数据库方便后期的维护.
比如用户有自己的特殊要求,可以单独定制一些功能。这些功能可以在他特有的版本内增加某些特殊的需求.

如何实现访问域名,跳转到对应的网站,这里有两种方法:

  • 反向代理
  • 路由

这里我更倾向于反向代理,因为每个用户有自己独立的程序,互不影响,不会因为一套系统挂了其他的商户跟着一起连坐。当然针对于 100 个用户的完全没有问题。因为程序是用 Nodejs 为后端,内存占用基本可以无视了.

反向代理这里我对 NGINX 还是比较熟悉的,这里就使用 OpenResty 跟 NGINX 基本一样,但是他支持 lua 模块,这里可以使用 lua 模块进行简单分流.
比如 当 域名AABBCC.example.com 或者 AABBCC.com 访问时,我只需要在某个地方找到对应的后端地址,把请求放过去。是不是有种 key-value 的感觉?嗯 对,就是 Redis, Redis 可以帮助我们做很多的事情.

既然解决了域名的问题,那 SSL 签名是不是也很重要?那显然啊,这样的服务当然要给网站上个绿锁了。这里选择 大名鼎鼎的
Let's Encrypt, 感谢提供免费 3 个月的 ssl 证书,当然还要考虑到续签的问题....

0x3 解决问题#

第一个问题当然就是 OpenResty 安装的问题,这里我选择的是宝塔面板,因为是很久远的 版本了。重点是 看一下 lua 模块是否合适。这里找不到以前写的一键脚本了... 各位看官动手能力应该就能解决.

下面讲解一下,如何实现域名的捆绑,首先要给自己一个泛域名,比如*.example.com, 这个域名用来承载AABBCC.example.com, 当访问本域名的时候要自动反向代理到 相应的后端.
根据前面提高的 key-value 原则,这里进行一次 key-value 查询操作,如果 ok 返回相应的端口并赋给下游的代理,如果没有那就返回错误.

 set $subdomain default;
    access_by_lua '
    local redis = require "resty.redis"
    local red = redis:new()
    red:set_timeout(1000)
    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
    ngx.say("failed to connect: ", err)
    return
    end
    local host = ngx.var.host;
    local res, err = red:get(host)
    if type(err)=="nil" then
    if res ~= ngx.null then
    ngx.log(ngx.INFO, "[web.xxx.fr]取出域名对应端口 => ", res)
    ngx.var.subdomain = "http://127.0.0.1:"..res
    else
    ngx.log(ngx.INFO, "[web.xxx.fr]端口未找到 => ", res)
    ngx.var.subdomain = "http://127.0.0.1:9998"
    end
    else
    ngx.log(ngx.INFO, "[web.xxx.fr]错误信息 => ", err)
    ngx.var.subdomain = "http://127.0.0.1:9998"
    end
    ';

    location / {
        proxy_pass $subdomain;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Origin "";
        add_header X-Cache $upstream_cache_status;
    }

至此为止,简单的分流就做好了.

如何自动开始绿锁呢,项目用的 node.js 来写的,虽然有个库叫做 GreenLock, 经过测试并没有适合我的项目.. 作为小项目的话 应该问题不大,至少是 4 年前 没有....

这里选择一个 https://github.com/auto-ssl/lua-resty-auto-ssl 这个库来实现。安装过程省略。这里选择 redis-adaptor, 这样所有证书会进入 redis, 随时可以更改。虽然这个东西有自动续期,当然对于我来说自动续期不是我自己期待的。要做到聪明的自动续期。于是就用 nodejs 写了个定时任务.

使用 lua-resty-auto-ssl 要是想自动续签,看了他们的源码,续签的时候需要拿到一个叫做 secret 的东西。于是就写了个这样的代码,每次都会拿到,最新的 secret, 然后发送给服务器进行续命操作.

const getToken = () => {
    return new Promise((resolve, reject) => {
        httpRequest.get(`http://127.0.0.1:${HOOK_SERVER_PORT}/getSecret`)
            .then((response) => {
                resolve(response.data.trim());
            }).catch((e) => {
            reject(e);
        });
    });
}

续签的命令,非常重要!!!要配置一下 hook 的服务器 并开放 8999 端口,这里也是通过 调用脚本实现的,目的是开启一个叫做 '/getSecret' 供下面续签命令执行

export HOOK_SERVER_PORT=8999;
export HOOK_SECRET=前面获取到的;
/resty-auto-ssl路径/dehydrated -c --accept-terms --no-lock -d 域名  --config xxxx -x --challenge http-01 --hook xxxx

每天晚上 无人的时候跑个定时任务,如果有需要续签的,就需要 reload 一下服务器。反正是脚本定时任务完成.

对了 续签的端口 为 8999 当然可以根据喜好看下配置来修改.
到此为止要求就做到了.

这样证书是在 redis 里面的,然后证书可以替换成 自己的证书。作为客户捆绑域名还是很简单的,直接 cname 到 你的服务器,服务器会自动处理这一些。单机部署还是舒服的.

0x4 总结#

其实在选择这方案之前 也考虑过 Caddy, 但是那时候的 Caddy 并不很稳定,续签经常出现卡顿,于是放弃。跑向啃源码的道路。但是正因为看了这样友好的开源项目,自己得到了不少提升。这个续签的方案,网上倒是有不少人提到,但是 自助脚本 hook 到 secret 进行主动续期 没人提过,稍微分享一下吧。有空了我上个 Demo, 因为光纤断了......

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。