阿里云证书文档地址:
`https://api.aliyun.com/document/cas/2020-04-07/overview`
创建证书步骤:
- 购买证书在阿里云控制台购买证书,选择免费版即可。地址为
`https://yundun.console.aliyun.com/?spm=5176.12818093_47.overview_recent.2.435a16d0Qv9jpF&p=cas#/certExtend/free/cn-hangzhou?currentPage=1&pageSize=10&keyword=&statusCode=` - 创建只拥有ssl管理权限的RAM账号
1)新增用户
2)赋权 - 申请创建证书,
查询证书
下载证书替换文件
这些利用go的定时任务来做
`go
`核心代码
package service
import (
"auto-update-ssl/pkg/config"
"auto-update-ssl/pkg/log"
"crypto/tls"
"encoding/json"
"fmt"
cas20200407 "github.com/alibabacloud-go/cas-20200407/v3/client"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
)
var (
clientOnce sync.Once
globalClient *cas20200407.Client
clientErr error
)
// 检查网站证书是否在三天内到期
func CheckCertificateExpiry(domain string) (bool, error) {
conf := &tls.Config{
InsecureSkipVerify: true, // 跳过证书验证,仅用于获取证书信息
}
conn, err := tls.Dial("tcp", domain+":443", conf)
if err != nil {
log.Log().Error(err)
return false, err
}
defer conn.Close()
certs := conn.ConnectionState().PeerCertificates
if len(certs) == 0 {
log.Log().Error("未找到证书")
return false, fmt.Errorf("未找到证书信息")
}
expiry := certs[0].NotAfter
threeDaysFromNow := time.Now().Add(3 * 24 * time.Hour)
return expiry.Before(threeDaysFromNow), nil
}
// 使用AK&SK初始化账号Client,带缓存
func GetClient() (*cas20200407.Client, error) {
clientOnce.Do(func() {
accessKeyID := config.GlobalConfig.Aliyun.AccessKeyID
if accessKeyID == "" {
log.Log().Error("阿里云访问密钥ID未设置")
clientErr = fmt.Errorf("阿里云访问密钥ID未设置")
return
}
accessKeySecret := config.GlobalConfig.Aliyun.AccessKeySecret
if accessKeySecret == "" {
log.Log().Error("阿里云访问密钥未设置")
clientErr = fmt.Errorf("阿里云访问密钥未设置")
return
}
c := &openapi.Config{
AccessKeyId: tea.String(accessKeyID),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("cas.aliyuncs.com"),
}
var err error
globalClient, err = cas20200407.NewClient(c)
clientErr = err
})
return globalClient, clientErr
}
//创建证书
func CreateCertificate() {
log.Log().Info("开始创建证书")
isExpire, err := CheckCertificateExpiry(config.GlobalConfig.Aliyun.Domain)
if err != nil {
log.Log().Error("检查证书过期状态失败", err)
return
}
if !isExpire {
log.Log().Info("证书未过期,无需替换")
return
}
client, err := GetClient()
if err != nil {
return
}
createCertificateForPackageRequestRequest := &cas20200407.CreateCertificateForPackageRequestRequest{
ProductCode: tea.String("digicert-free-1-free"),
Username: tea.String(config.GlobalConfig.Aliyun.UserName),
Phone: tea.String(config.GlobalConfig.Aliyun.Phone),
Email: tea.String(config.GlobalConfig.Aliyun.Email),
Domain: tea.String(config.GlobalConfig.Aliyun.Domain),
ValidateType: tea.String(config.GlobalConfig.Aliyun.ValidateType),
}
runtime := &util.RuntimeOptions{}
_, err = client.CreateCertificateForPackageRequestWithOptions(createCertificateForPackageRequestRequest, runtime)
if err != nil {
handlerErr(err)
}
}
// ListUserCertificateOrder 获取用户证书订单列表
func ListUserCertificateOrder() ([]*cas20200407.ListUserCertificateOrderResponseBodyCertificateOrderList, error) {
client, err := GetClient()
if err != nil {
return nil, err
}
listUserCertificateOrderRequest := &cas20200407.ListUserCertificateOrderRequest{
OrderType: tea.String("CERT"),
Status: tea.String("ISSUED"),
}
runtime := &util.RuntimeOptions{}
res, err := client.ListUserCertificateOrderWithOptions(listUserCertificateOrderRequest, runtime)
if err != nil {
return nil, err
}
return res.Body.CertificateOrderList, nil
}
// getUserCertificateDetail 通过证书ID获取用户证书详情
func getUserCertificateDetail(certId int64) (*cas20200407.GetUserCertificateDetailResponseBody, error) {
client, err := GetClient()
if err != nil {
return nil, err
}
getUserCertificateDetailRequest := &cas20200407.GetUserCertificateDetailRequest{
CertId: tea.Int64(certId),
}
runtime := &util.RuntimeOptions{}
res, err := client.GetUserCertificateDetailWithOptions(getUserCertificateDetailRequest, runtime)
if err != nil {
return nil, err
}
return res.Body, nil
}
func ReplaceCertificate() {
log.Log().Info("开始替换证书")
isExpire, err := CheckCertificateExpiry(config.GlobalConfig.Aliyun.Domain)
if err != nil {
log.Log().Error("检查证书过期状态失败", err)
return
}
if !isExpire {
log.Log().Info("证书未过期,无需替换")
return
}
list, err := ListUserCertificateOrder()
if err != nil {
log.Log().Error("获取用户证书订单列表失败", err)
return
}
if len(list) == 0 {
log.Log().Error("未找到有效的证书订单")
return
}
var (
certId int64
domain = config.GlobalConfig.Aliyun.Domain
maxStartDay = "2025-01-01"
)
//fmt.Println(list)
for _, v := range list {
if *v.CommonName == domain && *v.StartDate >= maxStartDay {
certId = *v.CertificateId
maxStartDay = *v.StartDate
}
}
detail, err := getUserCertificateDetail(certId)
if err != nil {
log.Log().Error("获取用户证书详情失败", err)
return
}
if detail == nil {
log.Log().Error("证书详情为空")
return
}
err = WriteStringToFile(config.GlobalConfig.Aliyun.KeyFile, *detail.Key)
if err != nil {
log.Log().Error("写入密钥文件失败", err)
return
}
err = WriteStringToFile(config.GlobalConfig.Aliyun.CertFile, *detail.Cert)
if err != nil {
log.Log().Error("写入证书文件失败", err)
return
}
log.Log().Info("证书替换成功")
log.Log().Info("是否重启 Nginx: ", config.GlobalConfig.RestartNginx)
if config.GlobalConfig.RestartNginx {
go restartNginx()
}
}
// WriteStringToFile 函数用于将字符串覆盖写入指定文件
func WriteStringToFile(filename string, content string) error {
dir := filepath.Dir(filename)
err := os.MkdirAll(dir, 0755)
if err != nil {
log.Log().Error(err)
return err
}
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Log().Error(err)
return err
}
defer file.Close()
_, err = io.WriteString(file, content)
if err != nil {
log.Log().Error(err)
return err
}
if config.GlobalConfig.RestartNginx {
go restartNginx()
}
return nil
}
func restartNginx() {
log.Log().Info("正在重启 Nginx...")
cmd := exec.Command("docker", "restart", "nginx")
// 运行命令并捕获输出和错误
output, err := cmd.CombinedOutput()
if err != nil {
log.Log().Error("重启 Nginx 失败: ", err, "输出: ", string(output))
} else {
log.Log().Info("重启 Nginx 成功, 输出: ", string(output))
}
}
func handlerErr(tryErr error) {
if tryErr != nil {
sdkErr := tryErr.(*tea.SDKError)
if sdkErr == nil {
sdkErr = &tea.SDKError{
Message: tea.String(tryErr.Error()),
Data: tea.String("{}"),
}
}
errMsg := tea.StringValue(sdkErr.Message)
log.Log().Error("Error message:", errMsg)
var data map[string]interface{}
d := json.NewDecoder(strings.NewReader(tea.StringValue(sdkErr.Data)))
if err := d.Decode(&data); err == nil {
if recommend, ok := data["Recommend"]; ok {
log.Log().Info("Recommendation:", recommend)
}
} else {
log.Log().Error("Failed to decode error data:", err)
}
_, assertErr := util.AssertAsString(sdkErr.Message)
if assertErr != nil {
log.Log().Error("AssertAsString failed:", assertErr)
}
}
}