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,因為光斷了......

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。