使用 API 上传 SSL 证书到七牛云
七牛云api 折腾心酸历程
搞起了网站,申请了 SSL 证书,设置了 https 协议。发现里面有一堆的东西要搞,不复杂,就是特别繁琐。
在宝塔中申请 SSL 证书特别简单,设置一下就行了。然后一看使用七牛云的对象存储要使用 https 也需要 SSL 证书,手动上传太麻烦了,于是想着直接用它的api 解决上传证书绑定域名,那么以后就不用麻烦了。
结果一搞,发现都是坑,七牛云的技术文档非常简略,而且指向不明,上传 SSL 证书需要管理凭证,但是它的管理凭证有两个页面,一个还是历史凭证。都这年都了谁还管历史凭证啊,用最新的!
于是被坑惨了,废了一天的时间,网络中还没几个相关的博文,也不知道是用户太少还是太坑,只能提个工单咨询,结果还是自己解决的。
总结七牛的技术文档和sdk ,文档上面写得是历史不算历史,sdk 中的废弃不是废弃,历史的还在用,废弃的也必须用,最新的反而用不了!
另外七牛只用 CDN 域名才能通过 api 绑定SSL证书,对象存储无法通过api 绑定证书,必须到七牛云控制台手动绑定证书。
提醒:七牛的 SSL 证书相关管理凭证,即 token 生成只能用旧的,最新的根本没用!
七牛云SSL 管理凭证生成及上传、绑定(java)
在写 java 代码进行验证时发现 vscode 真不错啊,又快又轻,除了智能提示差点外,其他都是优点,尤其是用来写 Demo ,运行超级快!
以下是java 代码,用来验证它的证书是否可行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.google.gson.Gson;
import com.qiniu.util.Auth;
import com.qiniu.util.StringUtils;
import com.qiniu.util.UrlSafeBase64;
import okhttp3.FormBody;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
public class UploadSSL {
// 七牛云Access Key和Secret Key
private static final String ACCESS_KEY = "MY_ACCESS_KEY";
private static final String SECRET_KEY = "MY_SECRET_KEY";
private static final String privateKeyFilePath = "/PATH/privkey.pem"; // 私钥文件路径
private static final String certBodyFilePath = "PATH/fullchain.pem"; // 证书链文件路径
// 七牛云管理接口地址
private static final String QINIU_API_HOST = "https://api.qiniu.com";
public static void main(String[] args) {
// -------------------------------------------
String certificateId = UploadQiniuSSL();
// ----------------------------------
if (certificateId.length() > 1) {
String domainToBind = "qiniu-cdn.yueproject.com";
// String certificateId = "68110966f23d4551688abf93";
System.out.println("Attempting to bind SSL certificate to domain: " +
domainToBind);
System.out.println("Using certificate ID: " + certificateId);
try {
String responseBody = bindCDNSslCertificate(domainToBind, certificateId, false,
false);
if (responseBody != null) {
System.out.println("SSL certificate bound successfully.");
System.out.println("Response body: " + responseBody); // 成功时通常返回 {}
} else {
System.out.println("Failed to bind SSL certificate.");
// 错误信息已在 bindSslCertificate 方法中打印
}
} catch (IOException e) {
e.printStackTrace();
System.err.println("An IOException occurred during the binding process.");
}
}
}
/**
* 纯粹 java 依赖
* 生成七牛云管理凭证,七牛部分接口可用
* 生成七牛云 Mac 认证所需的 Authorization 头
* 参考文档:
*
* @param requestUrl 请求的完整 URL
* @param method HTTP 方法 (GET, POST, PUT等)
* @param contentType 请求的 Content-Type (如果存在请求体)
* @param body 请求体字节数组 (如果存在请求体)
* @return Authorization 头字符串
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
private static String generateQiniuAuthorization(String requestUrl, String method, String contentType, byte[] body)
throws Exception {
URI uri = URI.create(requestUrl);
String path = uri.getRawPath();
String query = uri.getRawQuery();
String signData = path;
// System.out.println("Sign Data:\n--" + signData + "--"); // 调试用
// 2. 使用 SecretKey 进行 HMAC-SHA1 加密
SecretKeySpec signingKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
mac.update(path.getBytes(Charset.forName("UTF-8")));
mac.update((byte) '\n');
byte[] data = mac.doFinal();
// byte[] rawHmac = mac.doFinal(signData.getBytes(StandardCharsets.UTF_8));
// 3. 对加密结果进行 Base64 编码
String digest = Base64.getUrlEncoder().encodeToString(data); //
// 使用URL安全的Base64编码
String authorization = "QBox " + ACCESS_KEY + ":" + digest;
// // 4. 组合 AccessKey 和编码后的签名
System.out.println("签名1:" + authorization);
return authorization;
}
/**
* 使用Qiniu java-sdk 方式
* 生成七牛云管理凭证,七牛部分接口可用
* 生成七牛云 Mac 认证所需的 Authorization 头
* 参考文档:
*
* @param requestUrl 请求的完整 URL
* @param method HTTP 方法 (GET, POST, PUT等)
* @param contentType 请求的 Content-Type (如果存在请求体)
* @param body 请求体字节数组 (如果存在请求体)
* @return Authorization 头字符串
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
private static String generateQiniuAuthorization2(String requestUrl) {
Auth auth = Auth.create(ACCESS_KEY, SECRET_KEY);
// String authorization = (String)auth.authorization(SSL_CERT_API_URL,
// bodyBytes,"application/json").get("Authorization");
String authorization = (String) auth.authorization(requestUrl).get("Authorization");
System.out.println(authorization);
// QBox JIdaSWW7wlRptHAyAfFVUYPzEYiqyO9ho8eelBOs:-K33-i-je465DqWOZqEq9BLfItM=
// String authorization = signRequest("/sslcert");
System.out.println(authorization);
return authorization;
}
private static String UploadQiniuSSL() {
String commonName = "yueproject.com"; // 证书绑定的域名,通常是你的主域名
String certName = "yueproject"; // 给证书起一个名字
// String privateKeyFilePath = "/path";
// String certBodyFilePath = "/path";
// String ACCESS_KEY = "ACCESS_KEY";
// String SECRET_KEY = "SECRET_KEY";
String SSL_CERT_API_URL = "https://api.qiniu.com/sslcert";
try {
String priPem = readFileContent(privateKeyFilePath);
String caPem = readFileContent(certBodyFilePath);
if (priPem == null || caPem == null) {
System.err.println("Failed to read certificate files.");
return "";
}
Map<String, String> certData = new HashMap<>();
certData.put("name", certName);
certData.put("common_name", commonName);
certData.put("pri", priPem);
certData.put("ca", caPem);
String jsonBody = com.qiniu.util.Json.encode(certData);
byte[] bodyBytes = jsonBody.getBytes(StandardCharsets.UTF_8);
String authorization = generateQiniuAuthorization2(SSL_CERT_API_URL);
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
RequestBody requestBody = toRequestBody(jsonBody, MediaType.get("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url(SSL_CERT_API_URL)
.post(requestBody)
.header("Authorization", authorization)
.header("Content-Type", "application/json")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "No error body";
System.err.println("Certificate upload failed: " + response.code() + " - " + response.message());
System.err.println("Error Body: " + errorBody);
return "";
}
if (response.code() == 200) {
String responseBody = response.body().string();
System.out.println("Certificate uploaded successfully:");
Gson gson = new Gson();
UploadSSLReult result = gson.fromJson(responseBody, UploadSSLReult.class);
// {"code":200,"error":"","certID":"6811097419a2040abaed0474"}
System.out.println(responseBody);
return result.certId;
}
}
} catch (Exception e) {
e.printStackTrace();
System.err.println("Error during certificate upload process: " + e.getMessage());
}
return "";
}
/**
* 七牛 sdk 中生成证书的底层方法
*
* @param path 网络请求的接口, 即 /sslcert
* @return
*/
public static String signRequest(String path) {
System.out.println("path:" + path);
Mac mac;
try {
byte[] sk = StringUtils.utf8Bytes(SECRET_KEY);
SecretKeySpec secretKeySpec = new SecretKeySpec(sk, "HmacSHA1");
mac = javax.crypto.Mac.getInstance("HmacSHA1");
mac.init(secretKeySpec);
} catch (GeneralSecurityException e) {
e.printStackTrace();
throw new IllegalArgumentException(e);
}
mac.update(StringUtils.utf8Bytes(path));
mac.update((byte) '\n');
String digest = UrlSafeBase64.encodeToString(mac.doFinal());
return ACCESS_KEY + ":" + digest;
}
/**
* 绑定 CDN 的证书中以及是否强制使用 https 和是否弃用 http2
* 修改七牛 CDN 的使用
*
* @param domainToBind 绑定证书的自定义域名
* @param certId 证书的ID
* @return API 响应体字符串,null 表示请求失败
* @throws IOException 如果发生网络或请求错误
*/
public static String bindCDNSslCertificate(String domainToBind, String certId, boolean forceHttps, boolean http2Enable)
throws IOException {
// 构建完整的 API 请求 URL
// 请根据实际情况调整这里的路径,CDN域名和对象存储域名API路径可能不同
String requestUrl = QINIU_API_HOST + "/domain/" + domainToBind + "/httpsconf";
// 构建请求体 (JSON格式)
// 请查阅文档确认请求体格式,这里是一个示例
String jsonBody = String.format(
"{\"certId\":\"%s\",\"forceHttps\":%b,\"http2Enable\":%b}",
certId,
forceHttps,
http2Enable);
MediaType JSON = MediaType.parse("application/json");
RequestBody body = RequestBody.create(jsonBody, JSON);
try {
// 生成认证头
String authorizationHeader = generateQiniuAuthorization(
requestUrl,
"PUT", // 绑定操作通常是PUT或POST
JSON.toString(), // Content-Type for signing
jsonBody.getBytes(StandardCharsets.UTF_8) // Body for signing
);
// 构建 OkHttp 请求
Request request = new Request.Builder()
.url(requestUrl)
.put(body) // 使用PUT方法
.header("Content-Type", JSON.toString())
.header("Authorization", authorizationHeader)
.build();
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
// 执行请求
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
// 请求失败,打印错误信息
System.err.println("Request failed: " + response.code() + " " +
response.message());
if (response.body() != null) {
String errorBody = response.body().string();
System.err.println("Error Body: " + errorBody);
// 七牛API错误信息通常在JSON体中,可能需要解析
}
// 根据七牛API文档,失败时可能返回特定的错误码和信息
return null; // 返回null表示失败
}
// 请求成功,读取响应体
if (response.body() != null) {
return response.body().string(); // 通常成功时返回空JSON {} 或其他信息
} else {
return ""; // 成功但没有响应体
}
}
} catch (Exception e) {
e.printStackTrace();
System.err.println("Error generating authorization header.");
return null;
}
}
public static RequestBody toRequestBody(String source, MediaType contentType) {
Charset charset = StandardCharsets.UTF_8;
MediaType finalContentType = contentType;
if (contentType != null) {
Charset resolvedCharset = contentType.charset();
if (resolvedCharset == null) {
charset = StandardCharsets.UTF_8;
String contentTypeString = contentType + "; charset=utf-8";
finalContentType = MediaType.parse(contentTypeString);
} else {
charset = resolvedCharset;
}
}
byte[] bytes = source.getBytes(charset);
return RequestBody.create(bytes, finalContentType);
}
private static String readFileContent(String filePath) {
try {
return new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8);
} catch (IOException e) {
System.err.println("Error reading file: " + filePath);
e.printStackTrace();
return null;
}
}
// {"code":200,"error":"","certID":"6811097419a2040abaed0474"}
static class UploadSSLReult {
public int code;
public String error;
public String certId;
}
}
七牛云SSL 管理凭证生成及上传、绑定(shell)
在 java 中验证通过后,就把它转成 shell 脚本,让服务器中的宝塔面板可以执行定时任务。
我是用了宝塔面板来管理服务器,所以 shell 脚本就放在宝塔面板相关文件夹下,即 /www/scripts , 如果没有可以自己创建。
shell 脚本如下:
qiniu_upload_ssl.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#!/bin/bash
# --- 配置区域 ---
# 你的七牛云 Access Key 和 Secret Key
# !! 安全警告: 直接将密钥写入脚本存在风险。建议使用更安全的方式管理,例如环境变量或配置文件。
QINIU_ACCESS_KEY="YOUR_QINIU_ACCESS_KEY"
QINIU_SECRET_KEY="YOUR_QINIU_SECRET_KEY"
# 你在宝塔面板中配置SSL的域名 (用于查找证书文件)
BAOTA_DOMAIN_NAME="example.com"
# 在七牛中需要绑定此证书的 CDN 自定义域名
QINIU_DOMAIN="qiniu.example.com"
# 在七牛中显示的通用名词
QINIU_COMMON_NAME="example"
# 宝塔SSL证书存放的基础路径
BAOTA_SSL_BASE_DIR="/www/server/panel/vhost/ssl"
# --- 脚本主要逻辑 ---
# 函数:记录日志并退出
log_error_exit() {
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $1" >&2
exit 1
}
# 函数:记录信息
log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
# 签名函数(带逐步打印),一般的shell 方法靠着 echo 输出到屏幕中来获取返回值,
# 所以方法中除了最后一行不应该有其他输出屏幕的东西
# 函数:生成七牛 QiniuMac 认证签名
# 参数:
# $1: Path (e.g., /sslcert)
# $2: key (e.g., QINIU_SECRET_KEY)
generate_qiniu_auth() {
local path="$1"
local key="$2"
# echo "===== 调试模式 ====="
# echo "原始路径: '$path'"
# # 1. 模拟 mac.update(path) —— 添加路径数据
# echo -n "步骤1 [mac.update(path)] 数据(HEX): "
# printf "%s" "$path" | xxd -p | tr -d '\n'
# echo " (长度: $(printf "%s" "$path" | wc -c)字节)"
# printf "%s" "$path" > /tmp/part1.bin
# # 2. 模拟 mac.update('\n') —— 添加换行符
# echo -n "步骤2 [mac.update('\\n')] 数据(HEX): "
# printf "\n" | xxd -p | tr -d '\n'
# echo " (长度: 1字节)"
# printf "\n" > /tmp/part2.bin
# # 3. 合并数据并计算HMAC
# echo "步骤3 合并后的数据(HEX): $(cat /tmp/part1.bin /tmp/part2.bin | xxd -p | tr -d '\n')"
# # 4. 计算HMAC-SHA1(分步演示)
# echo -n "步骤4 HMAC-SHA1 二进制结果(HEX): "
# # 此处的 > /dev/null 是将此处命令行的输出丢弃, 因为shell 方法一般用输出到屏幕中做参数,若是调试,那么可以将此删除
# cat /tmp/part1.bin /tmp/part2.bin | openssl dgst -sha1 -hmac "$key" -binary | \
# xxd -p | tr -d '\n' > /dev/null
# echo
# # 5. Base64编码并转换
# local sign=$(cat /tmp/part1.bin /tmp/part2.bin | \
# openssl dgst -sha1 -hmac "$key" -binary | \
# base64 | tr -d '\n' | tr '+/' '-_')
# echo "步骤5 Base64结果: $sign"
# echo "===================="
local sign=$(printf "%s\n" "$path" | \
openssl dgst -sha1 -hmac "$key" -binary | \
base64 | tr -d '\n' | tr '+/' '-_')
echo "QBox ${QINIU_ACCESS_KEY}:${sign}"
}
# --- 检查配置和环境 ---
if [ -z "$QINIU_ACCESS_KEY" ] || [ "$QINIU_ACCESS_KEY" == "YOUR_QINIU_ACCESS_KEY" ]; then
log_error_exit "请在脚本中设置 QINIU_ACCESS_KEY。"
fi
if [ -z "$QINIU_SECRET_KEY" ] || [ "$QINIU_SECRET_KEY" == "YOUR_QINIU_SECRET_KEY" ]; then
log_error_exit "请在脚本中设置 QINIU_SECRET_KEY。"
fi
if [ -z "$BAOTA_DOMAIN_NAME" ]; then
log_error_exit "请在脚本中设置 BAOTA_DOMAIN_NAME。"
fi
if [ -z "$QINIU_DOMAIN" ]; then
log_error_exit "请在脚本中设置 QINIU_DOMAIN"
fi
# 检查必要命令是否存在
for cmd in curl jq openssl base64 tr; do
if ! command -v $cmd &> /dev/null; then
log_error_exit "命令 '$cmd' 未找到,请先安装它。"
fi
done
# --- 定位证书文件 ---
CERT_FILE_PATH="${BAOTA_SSL_BASE_DIR}/${BAOTA_DOMAIN_NAME}/fullchain.pem"
KEY_FILE_PATH="${BAOTA_SSL_BASE_DIR}/${BAOTA_DOMAIN_NAME}/privkey.pem"
log_info "查找证书文件: $CERT_FILE_PATH"
log_info "查找私钥文件: $KEY_FILE_PATH"
if [ ! -f "$CERT_FILE_PATH" ]; then
log_error_exit "证书文件未找到: $CERT_FILE_PATH"
fi
if [ ! -f "$KEY_FILE_PATH" ]; then
log_error_exit "私钥文件未找到: $KEY_FILE_PATH"
fi
# --- 读取证书和私钥内容 ---
# 使用 jq 来安全地处理可能的多行和特殊字符,并生成 JSON 字符串
CERT_CONTENT=$(jq -Rs . "$CERT_FILE_PATH")
KEY_CONTENT=$(jq -Rs . "$KEY_FILE_PATH")
# 检查内容是否读取成功
if [ -z "$CERT_CONTENT" ] || [ "$CERT_CONTENT" == "null" ]; then
log_error_exit "无法读取证书文件内容或文件为空。"
fi
if [ -z "$KEY_CONTENT" ] || [ "$KEY_CONTENT" == "null" ]; then
log_error_exit "无法读取私钥文件内容或文件为空。"
fi
# --- 步骤 1: 上传 SSL 证书到七牛云 ---
log_info "准备上传证书到七牛云..."
UPLOAD_HOST="api.qiniu.com"
UPLOAD_PATH="/sslcert"
UPLOAD_CONTENT_TYPE="application/json"
UPLOAD_METHOD="POST"
# 给证书起个名字 (可选,但推荐)
CERT_NAME="${BAOTA_DOMAIN_NAME}-$(date +%Y%m%d%H%M%S)"
# 构建 JSON 请求体
# 注意:这里直接使用之前 jq 处理过的包含引号的字符串
UPLOAD_BODY=$(cat <<EOF
{
"name": "$CERT_NAME",
"common_name": "$QINIU_COMMON_NAME",
"pri": $KEY_CONTENT,
"ca": $CERT_CONTENT
}
EOF
)
# 生成上传请求的 Authorization Header
UPLOAD_AUTH_HEADER=$(generate_qiniu_auth "$UPLOAD_PATH" "$QINIU_SECRET_KEY" )
log_info "正在上传证书..."
# -s 静默模式, -w "%{http_code}" 输出HTTP状态码
UPLOAD_RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
-X "$UPLOAD_METHOD" \
-H "Host: $UPLOAD_HOST" \
-H "Content-Type: $UPLOAD_CONTENT_TYPE" \
-H "Authorization: $UPLOAD_AUTH_HEADER" \
--data-raw "$UPLOAD_BODY" \
"https://$UPLOAD_HOST$UPLOAD_PATH")
# 分离响应体和状态码
HTTP_STATUS=$(echo "$UPLOAD_RESPONSE" | grep "HTTP_STATUS:" | cut -d':' -f2)
RESPONSE_BODY=$(echo "$UPLOAD_RESPONSE" | sed '$d') # 删除最后一行状态码
# 检查上传结果
if [ "$HTTP_STATUS" != "200" ]; then
log_error_exit "上传证书失败。HTTP状态码: $HTTP_STATUS, 响应: $RESPONSE_BODY"
fi
# 从响应中解析证书 ID
CERT_ID=$(echo "$RESPONSE_BODY" | jq -r '.certID')
if [ -z "$CERT_ID" ] || [ "$CERT_ID" == "null" ]; then
log_error_exit "上传证书成功,但无法从响应中解析 certID。响应: $RESPONSE_BODY"
fi
log_info "证书上传成功!证书名称: $CERT_NAME, 证书 ID: $CERT_ID"
# ------------------- 绑定 CDN 证书 -------------------------------------------------
# --- 步骤 2: 将证书绑定到七牛云 自定义域名 ---
log_info "准备将证书绑定到 域名: $QINIU_DOMAIN ..."
BIND_HOST="api.qiniu.com"
BIND_PATH="/domain/${QINIU_DOMAIN}/httpsconf"
BIND_CONTENT_TYPE="application/json"
BIND_METHOD="PUT"
# 构建 JSON 请求体 (启用 HTTPS,不强制跳转)
BIND_BODY=$(cat <<EOF
{
"certId": "$CERT_ID",
"forceHttps": false,
"http2Enable": true
}
EOF
)
# 生成绑定请求的 Authorization Header
BIND_AUTH_HEADER=$(generate_qiniu_auth "$BIND_PATH" "$QINIU_SECRET_KEY" )
log_info "正在绑定证书 $CERT_ID 到域名..."
BIND_RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
-X "$BIND_METHOD" \
-H "Host: $BIND_HOST" \
-H "Content-Type: $BIND_CONTENT_TYPE" \
-H "Authorization: $BIND_AUTH_HEADER" \
--data-raw "$BIND_BODY" \
"https://$BIND_HOST$BIND_PATH")
# 分离响应体和状态码
HTTP_STATUS=$(echo "$BIND_RESPONSE" | grep "HTTP_STATUS:" | cut -d':' -f2)
RESPONSE_BODY=$(echo "$BIND_RESPONSE" | sed '$d') # 删除最后一行状态码
# 检查绑定结果 (通常成功是 200 OK)
if [ "$HTTP_STATUS" != "200" ]; then
# 尝试解析可能的错误信息
ERROR_MSG=$(echo "$RESPONSE_BODY" | jq -r '.error // ""')
log_error_exit "绑定证书到域名 $QINIU_DOMAIN 失败。HTTP状态码: $HTTP_STATUS, 错误信息: ${ERROR_MSG:-$RESPONSE_BODY}"
else
log_info "绑定证书到域名 $QINIU_DOMAIN 成功。HTTP状态码: 200 $RESPONSE_BODY"
fi
log_info "证书成功绑定到域名: $QINIU_DOMAIN !"
log_info "请注意:七牛云 配置生效可能需要几分钟时间。"
# --- 完成 ---
exit 0
shell 脚本记得使用 chmod +x qiniu_upload_ssh.sh 让脚本可以直接执行,不然会出现 curl 之类的软件找不到的问题。
check_upload_ssl.sh
check_upload_ssl.sh 脚本是用于检查宝塔是否续签了证书,它在运行的时候记录下证书的 SHA256 哈希值,然后等下次判断是否相同,如果不同则会运行 qiniu_upload_ssh.sh 脚本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#!/bin/bash
#
# 脚本用途:监控一个证书文件,如果内容发生变化则执行另一个脚本
#
# 如何使用:
# 1. 配置下面的三个变量:MONITORED_CERT_FILE, STATE_DIR, ACTION_SCRIPT
# 2. 赋予脚本执行权限:chmod +x check_cert_change.sh
# 3. 将脚本添加到 cron 定时任务中,例如每天执行一次。
# --- 配置变量 ---
# 要监控的证书文件完整路径(例如:fullchain.pem)
DOMAIN="your_domain.com"
MONITORED_CERT_FILE="/www/server/panel/vhost/ssl/$DOMAIN/fullchain.pem"
# 存放状态文件(记录上一次哈希值)的目录,请确保此目录存在且脚本有写入权限
# 例如:/var/lib/my_monitor 或者 /opt/cert_monitor_state
STATE_DIR="/www/scripts/cert_monitor"
# 状态文件的完整路径(自动生成,通常不需要修改)
# 使用证书文件名作为状态文件的一部分名称,以便监控多个文件
STATE_FILE="$STATE_DIR/$(basename "$MONITORED_CERT_FILE").sha256sum"
# 证书改变时要执行的另一个脚本的完整路径,请确保此脚本存在且可执行
ACTION_SCRIPT="/www/scripts/qiniu_upload_ssl.sh"
# 日志文件路径,用于记录脚本的运行状态和错误,请确保脚本有写入权限
LOG_FILE="/www/scripts/cert_monitor.log"
# --- 辅助函数和主要逻辑 ---
# 获取当前时间戳
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
# 日志记录函数
log() {
echo "$TIMESTAMP [INFO] $(basename "$0"): $1" | tee -a "$LOG_FILE"
}
# 错误日志记录函数
log_error() {
echo "$TIMESTAMP [ERROR] $(basename "$0"): $1" | tee -a "$LOG_FILE" >&2 # 同时输出到标准错误
}
# --- 脚本主要执行流程 ---
log "脚本开始运行..."
# 1. 检查被监控的证书文件是否存在
if [ ! -f "$MONITORED_CERT_FILE" ]; then
log_error "错误:被监控的证书文件不存在: $MONITORED_CERT_FILE"
exit 1 # 退出并标记失败
fi
# 2. 确保存放状态文件的目录存在并有权限
mkdir -p "$STATE_DIR"
if [ $? -ne 0 ]; then
log_error "错误:无法创建或访问状态文件目录: $STATE_DIR"
exit 1 # 退出并标记失败
fi
# 3. 计算当前证书文件的 SHA256 哈希值
# 使用 2>/dev/null 忽略 sha256sum 可能的错误输出
CURRENT_HASH=$(sha256sum "$MONITORED_CERT_FILE" 2>/dev/null | awk '{print $1}')
if [ -z "$CURRENT_HASH" ]; then
log_error "错误:无法计算证书文件 ($MONITORED_CERT_FILE) 的哈希值。"
exit 1 # 退出并标记失败
fi
log "当前证书哈希: $CURRENT_HASH"
# 4. 检查状态文件是否存在,如果存在则读取上一次的哈希值
PREVIOUS_HASH=""
if [ -f "$STATE_FILE" ]; then
PREVIOUS_HASH=$(cat "$STATE_FILE" 2>/dev/null)
log "上一次记录的哈希: ${PREVIOUS_HASH:-无}" # 如果为空则显示“无”
else
log "状态文件不存在 ($STATE_FILE),这可能是第一次运行或状态文件丢失。"
fi
# 5. 比较当前哈希值与上一次记录的哈希值
if [ "$CURRENT_HASH" != "$PREVIOUS_HASH" ]; then
log "检测到证书文件内容发生变化!旧哈希: ${PREVIOUS_HASH:-无} -> 新哈希: $CURRENT_HASH"
# 6. 执行指定的动作脚本
log "正在执行动作脚本: $ACTION_SCRIPT"
# 检查动作脚本是否存在且可执行
if [ -x "$ACTION_SCRIPT" ]; then
"$ACTION_SCRIPT" # <-- 调用您的另一个脚本
ACTION_SCRIPT_EXIT_STATUS=$? # 获取动作脚本的退出状态码
if [ $ACTION_SCRIPT_EXIT_STATUS -eq 0 ]; then
log "动作脚本执行成功。"
else
log_error "警告:动作脚本执行失败,退出状态码: $ACTION_SCRIPT_EXIT_STATUS"
# 注意:即使动作脚本失败,我们通常也更新状态文件,避免下次重复触发
fi
else
log_error "错误:动作脚本不存在或不可执行: $ACTION_SCRIPT"
# 如果动作脚本不可执行,不更新状态文件,下次会再次尝试执行
exit 1 # 退出并标记失败,表示主要任务(执行动作)失败
fi
# 7. 更新状态文件,将当前的哈希值保存下来
echo "$CURRENT_HASH" > "$STATE_FILE"
if [ $? -ne 0 ]; then
log_error "错误:无法更新状态文件: $STATE_FILE。下次运行时可能再次触发脚本。"
# 注意:状态文件更新失败是一个问题,但通常不会导致脚本退出,只是记录错误
fi
else
# 哈希值相同,表示文件没有改变
# log "证书文件未改变。" # 如果需要频繁运行并记录,可以取消此注释
: # 无操作
fi
# 8. 脚本运行结束
log "脚本运行结束。"
exit 0 # 成功退出
宝塔设置定时任务
在宝塔面板中设置计划任务,将 check_upload_ssl.sh 的完整路径添加到计划任务中,让它每天运行,这样就一切结束了。
运行链是 宝塔计划任务 -> check_upload_ssl.sh -> qiniu_upload_ssl.sh
总结
以上就是使用七牛 api 上传 SSL 证书的全部内容。只能说搞得太艰辛了,七牛在外面的资料太少了,也可能是现在大家都很少写博客了,让我废了好大的劲才整完所有流程。
七牛的对象存储域名不能直接绑定 SSL 证书真是不当人啊!