寶塔面板配置Let’s Encrypt證書自動續簽失效及解決方案

一、背景小故事

筆者手裡有個朋友交給我去日常運維項目是PHP+微信小程式,部署在Linux系統上。

這個項目是用寶塔面板去進行日常的可視化運維管理,用起來蠻香的。

如不清楚寶塔的同學,可以自行了解,這裡就不詳細說明。

寶塔是一款簡單好用的伺服器運維面板,並且永久免費。

我們都知道, 小程式請求的後端介面,要求是https協議的。所以後端伺服器得配置上SSL證書。

我接手之前,這個項目的SSL證書是直接購買的,而且也要到期了。當時,我對寶塔面板也是第一次接觸,不是特熟悉。經過一番摸索,看到寶塔面板提供Let’s Encrypt這樣免費的證書。這個證書有個缺點就是只有3個月的免費期限,到期後需要再去續期。

SSL證書配置

經過一番折騰,就給網站安排上了這個免費的證書。而且寶塔面板在這裡也有明確提示:將在距離到期時間一個月內嘗試自動續簽

看到那個提示後,發現這個證書可以一直免費使用,那倒是省錢又省心了。

隔了3個月後,老闆發來一條消息說:網站不正常了,給我看看唄

經過我一頓熟悉操作分析,排查伺服器,排查應用,並利用fiddler抓包工具去進行抓包分析後,確定是Https協議到期導致的問題。

然後我在SSL配置介面上,手動去點續簽,等了一會兒,續簽成功。網站又可以正常訪問,告訴老闆完美解決。

又經過一段時間後,老闆又發來一條消息說,網站又不正常,再給我看看什麼問題。

我一看到消息,知道又翻車了。不過這次我是輕車熟路,直接去手動點了一下續簽,解決。

……

就這樣,重複了很多次。

這樣長久下去,也不是辦法。

二、萌動想法

我得想出一個法子來解決這個問題。畢竟我們都是一枚程式設計師,專門去解決生活中出現的重複勞動力。

既然點一下續簽,就可以解決證書到期問題。那我們能不能在程式里用定時任務+模擬請求來自動續簽?

我就開始構想一下實現思路:

第一步:我們需要拿到續簽按鈕觸發的後端服務介面及請求參數,後續能模擬請求。

第二步:驗證介面是否可以直接請求成功,是否需要許可權驗證?經過驗證,需要先登陸,才能請求成功。

第三步:還需要一個定時任務功能。經過確認 寶塔面板自帶有任務計劃功能。

帶著這樣的想法,開始去嘗試實現,過程中有遇到很多問題,就不詳細說明,主要都是在寫Shell腳本構造請求參數傳遞。

我就直接上解決方案供同學們參考。

三、方案實踐

3.1 找到續簽請求介面

介面地址://IP:8888/ssl?action=Renew_SSL

續簽請求介面

如果我們直接請求該介面,會發現需要登陸,不能直接請求成功。

3.2 設置寶塔 API介面

經過查閱資料,寶塔面板提供了API介面,用密鑰key生成token後,再發起請求就可以,而不需要用戶名和密碼。我們能方便直接使用寶塔裡面的任何API介面。

面板設置 =》打開 API介面,拿到密鑰key,並配置IP白名單。IP可以直接添加伺服器IP。

第一步API介面打開

第二步拿到密鑰和配置IP白名單

而且寶塔也提供了多個版本的API介面 Demo樣例,可以很方便的集成。
樣例地址://www.bt.cn/bbs/thread-20376-1-1.html

API介面文檔

我比較熟悉Java,也就下載的JavaDemo研究的。

package com.raysonfang.bt.test;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.security.MessageDigest;

/**
 * 寶塔API測試
 *
 * @author fanglei
 * @date 2021/02/21 10:44
 **/
public class BTTest {
    public static void main(String[] args)
    {
        try {
            String btSign = "寶塔API密鑰";
            String url = "//IP:8888/ssl?action=Renew_SSL";
            String timestamp = System.currentTimeMillis() + "";
            String md5Sign = getMd5(btSign);
            String temp = timestamp+md5Sign;
            String token = getMd5(temp);
            String json = "request_time="+timestamp+"&request_token="+token;
            String responseText = sendPost(url,json);
            System.out.println(responseText);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public static String getMd5(String str) throws Exception
    {
        try {
            // 生成一個MD5加密計算摘要
            MessageDigest md = MessageDigest.getInstance("MD5");
            // 計算md5函數
            md.update(str.getBytes());
            // digest()最後確定返回md5 hash值,返回值為8為字元串。因為md5 hash值是16位的hex值,實際上就是8位的字元
            // BigInteger函數則將8位的字元串轉換成16位hex值,用字元串來表示;得到字元串形式的hash值
            return new BigInteger(1, md.digest()).toString(16);
        } catch (Exception e) {
            throw new Exception("MD5加密出現錯誤,"+e.toString());
        }
    }
    
    public static String sendPost(String url, String param) {
        PrintWriter out = null;
        BufferedReader in = null;
        StringBuffer result = new StringBuffer();
        try {
            URL realUrl = new URL(url);
            // 打開和URL之間的連接
            URLConnection conn = realUrl.openConnection();
            // 設置通用的請求屬性
            conn.setRequestProperty("accept", "text/xml,text/javascript,text/html,application/json");
            conn.setRequestProperty("connection", "Keep-Alive");
            // 發送POST請求必須設置如下兩行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 獲取URLConnection對象對應的輸出流
            out = new PrintWriter(conn.getOutputStream());
            // 發送請求參數
            out.print(param);
            // flush輸出流的緩衝
            out.flush();
            // 定義BufferedReader輸入流來讀取URL的響應
            in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result.append(line);
            }
        } catch (Exception e) {
            System.out.println("發送 POST 請求出現異常!"+e);
            e.printStackTrace();
        }
        //使用finally塊來關閉輸出流、輸入流
        finally{
            try{
                if(out!=null){
                    out.close();
                }
                if(in!=null){
                    in.close();
                }
            }
            catch(IOException ex){
                ex.printStackTrace();
            }
        }
        return result.toString();
    }
}

下載demo 研究請求參數構成,並調試成功。

3.3 設置定時任務,模擬請求

寶塔 直接提供有計劃任務功能,我們先看看能不能實現我們想要的功能,如果不能,我們再想其他辦法解決。

計劃任務

我看了任務類型有:Shell腳本, 備份網站,備份資料庫,日誌切割,釋放記憶體,訪問URL。

其中 Shell腳本訪問URL這兩種任務類型跟我們想要的很接近,其他的都不怎麼適合。

訪問URL這種類型也可以排除,是因為這裡採用直接配置URL,適合無動態參數,無許可權驗證的URL。

那剩下的就只有Shell腳本來實現。

我們知道Shell腳本也是一種程式語言腳本,那我們就用它來模擬請求了。

經過幾個小時的研究,把shell腳本寫出來。還是很費勁,對於Shell腳本里的參數傳遞語法不怎麼熟悉,也反覆去嘗試,才摸索清楚。

#!/bin/bash

# 獲取時間戳
cur_timestamp=$((`date '+%s'`*1000+`date '+%N'`/1000000))
# 寶塔密鑰
api_sk='uSth3rmADQ9Np5Zyhxxxxxxxxxxxxxxx'
# 密鑰MD5加密
key=`echo -n $api_sk|md5sum|cut -d" " -f1`
# 生成token
request_token=`echo -n $cur_timestamp$key|md5sum|cut -d" " -f1`
# 構造請求參數,並通過curl發送請求
curl -i -X POST -d "request_token=$request_token&request_time=$cur_timestamp" //ip:8888/ssl?action=Renew_SSL

把Shell腳本的密鑰和IP進行替換,就可以直接去任務計劃添加上,然後手動執行一下看看 是否可以運行成功

注意:第一次添加任務後,需要手動點執行,並在日誌去查看是否執行成功

任務執行成功

至此,以後可以放心交給程式自動續簽。

四、總結

也許官方已經解決了自動續簽的問題,而我這個也許是個偏方,但這裡面包含抓包,定時任務,介面鑒權,Shell腳本等知識運用