一、背景小故事
筆者手里有個(gè)朋友交給我去日常運(yùn)維項(xiàng)目是PHP+微信小程序,部署在Linux系統(tǒng)上。
這個(gè)項(xiàng)目是用寶塔面板去進(jìn)行日常的可視化運(yùn)維管理,用起來(lái)蠻香的。
如不清楚寶塔的同學(xué),可以自行了解,這里就不詳細(xì)說(shuō)明。
我們都知道, 小程序請(qǐng)求的后端接口,要求是https協(xié)議的。所以后端服務(wù)器得配置上SSL證書。
我接手之前,這個(gè)項(xiàng)目的SSL證書是直接購(gòu)買的,而且也要到期了。當(dāng)時(shí),我對(duì)寶塔面板是第一次接觸,不是特熟悉。經(jīng)過(guò)一段時(shí)間摸索,看到寶塔面板提供Let's Encrypt免費(fèi)的SSL證書。這個(gè)證書有個(gè)缺點(diǎn)就是只有3個(gè)月的免費(fèi)期限,到期后需要再去續(xù)期。

經(jīng)過(guò)一番折騰,就給網(wǎng)站安排上了這個(gè)免費(fèi)的證書。而且寶塔面板在這里也有明確提示:將在距離到期時(shí)間一個(gè)月內(nèi)嘗試自動(dòng)續(xù)簽。
看到那個(gè)提示后,發(fā)現(xiàn)這個(gè)證書可以一直免費(fèi)使用,那倒是省錢又省心了。
隔了3個(gè)月后,老板發(fā)來(lái)一條消息說(shuō):網(wǎng)站不正常了,給我看看唄。
經(jīng)過(guò)我一頓熟悉操作分析,排查服務(wù)器,排查應(yīng)用,并利用fiddler抓包工具去進(jìn)行抓包分析后,確定是Https協(xié)議到期導(dǎo)致的問(wèn)題。
然后我在SSL配置界面上,手動(dòng)去點(diǎn)了一下續(xù)簽,等了一會(huì)兒,續(xù)簽成功,網(wǎng)站又可以正常訪問(wèn),告訴老板完美解決。
又經(jīng)過(guò)一段時(shí)間后,老板又發(fā)來(lái)一條消息說(shuō),網(wǎng)站又不正常,再給我看看什么問(wèn)題。
我一看到消息,知道又翻車了。不過(guò)這次我是輕車熟路,直接去手動(dòng)點(diǎn)了一下續(xù)簽,解決。
......
就這樣,重復(fù)了很多次。
這樣長(zhǎng)久下去,也不是辦法。
二、萌動(dòng)想法
我得想出一個(gè)法子來(lái)解決這個(gè)問(wèn)題。畢竟我們都是一枚程序員,專門去解決生活中出現(xiàn)的重復(fù)勞動(dòng)力。
既然點(diǎn)一下續(xù)簽,就可以解決證書到期問(wèn)題。那我們能不能在程序里用定時(shí)任務(wù)+模擬請(qǐng)求來(lái)自動(dòng)續(xù)簽?
我就開(kāi)始構(gòu)想一下實(shí)現(xiàn)思路:
第一步:我們需要拿到續(xù)簽按鈕觸發(fā)的后端服務(wù)接口及請(qǐng)求參數(shù),后續(xù)能模擬請(qǐng)求。
第二步:驗(yàn)證接口是否可以直接請(qǐng)求成功,是否需要權(quán)限驗(yàn)證?經(jīng)過(guò)驗(yàn)證,需要先登陸,才能請(qǐng)求成功。
第三步:還需要一個(gè)定時(shí)任務(wù)功能。經(jīng)過(guò)確認(rèn) 寶塔面板自帶有任務(wù)計(jì)劃功能。
帶著這樣的想法,開(kāi)始去嘗試實(shí)現(xiàn),過(guò)程中有遇到很多問(wèn)題,就不詳細(xì)說(shuō)明,主要都是在寫Shell腳本構(gòu)造請(qǐng)求參數(shù)傳遞。
我就直接上解決方案供同學(xué)們參考。
三、方案實(shí)踐
3.1 找到續(xù)簽請(qǐng)求接口
接口地址:http://IP:8888/ssl?action=Renew_SSL

如果我們直接請(qǐng)求該接口,會(huì)發(fā)現(xiàn)需要登陸,不能直接請(qǐng)求成功。
3.2 設(shè)置寶塔 API接口
經(jīng)過(guò)查閱資料,寶塔面板提供了API接口,用密鑰key生成token后,再發(fā)起請(qǐng)求就可以,而不需要用戶名和密碼。我們能方便直接使用寶塔里面的任何API接口。
面板設(shè)置 =》打開(kāi) API接口,拿到密鑰key,并配置IP白名單。IP可以直接添加服務(wù)器IP。


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

我比較熟悉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測(cè)試
*
* @author fanglei
* @date 2021/02/21 10:44
**/
public class BTTest {
public static void main(String[] args)
{
try {
String btSign = "寶塔API密鑰";
String url = "http://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 {
// 生成一個(gè)MD5加密計(jì)算摘要
MessageDigest md = MessageDigest.getInstance("MD5");
// 計(jì)算md5函數(shù)
md.update(str.getBytes());
// digest()最后確定返回md5 hash值,返回值為8為字符串。因?yàn)閙d5 hash值是16位的hex值,實(shí)際上就是8位的字符
// BigInteger函數(shù)則將8位的字符串轉(zhuǎn)換成16位hex值,用字符串來(lái)表示;得到字符串形式的hash值
return new BigInteger(1, md.digest()).toString(16);
} catch (Exception e) {
throw new Exception("MD5加密出現(xiàn)錯(cuò)誤,"+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);
// 打開(kāi)和URL之間的連接
URLConnection conn = realUrl.openConnection();
// 設(shè)置通用的請(qǐng)求屬性
conn.setRequestProperty("accept", "text/xml,text/javascript,text/html,application/json");
conn.setRequestProperty("connection", "Keep-Alive");
// 發(fā)送POST請(qǐng)求必須設(shè)置如下兩行
conn.setDoOutput(true);
conn.setDoInput(true);
// 獲取URLConnection對(duì)象對(duì)應(yīng)的輸出流
out = new PrintWriter(conn.getOutputStream());
// 發(fā)送請(qǐng)求參數(shù)
out.print(param);
// flush輸出流的緩沖
out.flush();
// 定義BufferedReader輸入流來(lái)讀取URL的響應(yīng)
in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result.append(line);
}
} catch (Exception e) {
System.out.println("發(fā)送 POST 請(qǐng)求出現(xiàn)異常!"+e);
e.printStackTrace();
}
//使用finally塊來(lái)關(guān)閉輸出流、輸入流
finally{
try{
if(out!=null){
out.close();
}
if(in!=null){
in.close();
}
}
catch(IOException ex){
ex.printStackTrace();
}
}
return result.toString();
}
}
下載demo 研究請(qǐng)求參數(shù)構(gòu)成,并調(diào)試成功。
3.3 設(shè)置定時(shí)任務(wù),模擬請(qǐng)求
寶塔 直接提供有計(jì)劃任務(wù)功能,我們先看看能不能實(shí)現(xiàn)我們想要的功能,如果不能,我們?cè)傧肫渌k法解決。

我看了任務(wù)類型有:Shell腳本, 備份網(wǎng)站,備份數(shù)據(jù)庫(kù),日志切割,釋放內(nèi)存,訪問(wèn)URL。
其中Shell腳本和訪問(wèn)URL這兩種任務(wù)類型跟我們想要的很接近,其他的都不怎么適合。
訪問(wèn)URL,這種類型也可以排除,是因?yàn)檫@里采用直接配置URL,適合無(wú)動(dòng)態(tài)參數(shù),無(wú)權(quán)限驗(yàn)證的URL。
那剩下的就只有Shell腳本來(lái)實(shí)現(xiàn)。
我們知道Shell腳本也是一種編程語(yǔ)言腳本,那我們就用它來(lái)模擬請(qǐng)求了。
經(jīng)過(guò)幾個(gè)小時(shí)的研究,把shell腳本寫出來(lái)。還是很費(fèi)勁,對(duì)于Shell腳本里的參數(shù)傳遞語(yǔ)法不怎么熟悉,也反復(fù)去嘗試,才摸索清楚。
#!/bin/bash
# 獲取時(shí)間戳
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`
# 構(gòu)造請(qǐng)求參數(shù),并通過(guò)curl發(fā)送請(qǐng)求
curl -i -X POST -d "request_token=$request_token&request_time=$cur_timestamp" http://ip:8888/ssl?action=Renew_SSL
把Shell腳本的密鑰和IP進(jìn)行替換,就可以直接去任務(wù)計(jì)劃添加上,然后手動(dòng)執(zhí)行一下看看 是否可以運(yùn)行成功

注意:第一次添加任務(wù)后,需要手動(dòng)點(diǎn)執(zhí)行,并在日志去查看是否執(zhí)行成功

至此,以后可以放心交給程序自動(dòng)續(xù)簽。
四、總結(jié)
也許官方已經(jīng)解決了自動(dòng)續(xù)簽的問(wèn)題,而我這個(gè)也許是個(gè)偏方,但是這里面還是包含抓包,定時(shí)任務(wù),接口鑒權(quán),Shell腳本等知識(shí)運(yùn)用。