12

宝塔面板配置Let's Encrypt证书自动续签失效及解决方案

 3 years ago
source link: http://www.cnblogs.com/werewolfBoy/p/14426135.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

一、背景小故事

笔者手里有个朋友交给我去日常运维项目是PHP+微信小程序,部署在Linux系统上。

这个项目是用宝塔面板去进行日常的可视化运维管理,用起来蛮香的。

如不清楚宝塔的同学,可以自行了解,这里就不详细说明。

宝塔是一款简单好用的服务器运维面板,并且永久免费。

我们都知道, 小程序请求的后端接口,要求是https协议的。所以后端服务器得配置上SSL证书。

我接手之前,这个项目的SSL证书是直接购买的,而且也要到期了。当时,我对宝塔面板也是第一次接触,不是特熟悉。经过一番摸索,看到宝塔面板提供Let's Encrypt这样免费的证书。这个证书有个缺点就是只有3个月的免费期限,到期后需要再去续期。

bAjARvn.png!mobile

经过一番折腾,就给网站安排上了这个免费的证书。而且宝塔面板在这里也有明确提示: 将在距离到期时间一个月内尝试自动续签

看到那个提示后,发现这个证书可以一直免费使用,那倒是省钱又省心了。

隔了3个月后,老板发来一条消息说: 网站不正常了,给我看看呗

经过我一顿熟悉操作分析,排查服务器,排查应用,并利用fiddler抓包工具去进行抓包分析后,确定是Https协议到期导致的问题。

然后我在SSL配置界面上,手动去点续签,等了一会儿,续签成功。网站又可以正常访问,告诉老板完美解决。

又经过一段时间后,老板又发来一条消息说,网站又不正常,再给我看看什么问题。

我一看到消息,知道又翻车了。不过这次我是轻车熟路,直接去手动点了一下续签,解决。

......

就这样,重复了很多次。

这样长久下去,也不是办法。

二、萌动想法

我得想出一个法子来解决这个问题。毕竟我们都是一枚程序员,专门去解决生活中出现的重复劳动力。

既然点一下续签,就可以解决证书到期问题。那我们能不能在程序里用 定时任务+模拟请求来自动续签?

我就开始构想一下实现思路:

第一步:我们需要拿到 续签 按钮触发的后端服务接口及请求参数,后续能模拟请求。

第二步:验证接口是否可以直接请求成功,是否需要权限验证?经过验证,需要先登陆,才能请求成功。

第三步:还需要一个定时任务功能。经过确认 宝塔面板自带有任务计划功能。

带着这样的想法,开始去尝试实现,过程中有遇到很多问题,就不详细说明,主要都是在写Shell脚本构造请求参数传递。

我就直接上解决方案供同学们参考。

三、方案实践

3.1 找到续签请求接口

接口地址: http://IP:8888/ssl?action=Renew_SSL

imMvyy3.png!mobile

如果我们直接请求该接口,会发现需要登陆,不能直接请求成功。

3.2 设置宝塔 API接口

经过查阅资料,宝塔面板提供了API接口,用密钥key生成token后,再发起请求就可以,而不需要用户名和密码。我们能方便直接使用宝塔里面的任何API接口。

面板设置 =》打开 API接口,拿到密钥key,并配置IP白名单。IP可以直接添加服务器IP。

j2iMvum.png!mobile

eABjm2a.png!mobile

而且宝塔也提供了多个版本的API接口 Demo样例,可以很方便的集成。

样例地址: https://www.bt.cn/bbs/thread-20376-1-1.html

viYVJnF.png!mobile

我比较熟悉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 = "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 {
            // 生成一个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 设置定时任务,模拟请求

宝塔 直接提供有计划任务功能,我们先看看能不能实现我们想要的功能,如果不能,我们再想其他办法解决。

aM7ryqe.png!mobile

我看了任务类型有: 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" http://ip:8888/ssl?action=Renew_SSL

把Shell脚本的密钥和IP进行替换,就可以直接去任务计划添加上,然后手动执行一下看看 是否可以运行成功

iea6j2R.png!mobile

注意:第一次添加任务后,需要手动点执行,并在日志去查看是否执行成功

yaEF73y.png!mobile

至此,以后可以放心交给程序自动续签。

四、总结

也许官方已经解决了自动续签的问题,而我这个也许是个偏方,但这里面包含 抓包,定时任务,接口鉴权,Shell脚本等知识运用


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK