gitee-webhook 部署测试
This commit is contained in:
parent
5f77193e57
commit
2c7a88f900
|
@ -1,41 +1,47 @@
|
|||
package com.dite.znpt.web.controller;
|
||||
package com.dite.znpt.web.build;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
@RestController
|
||||
public class DeployController {
|
||||
|
||||
@Value("${deploy.secret}")
|
||||
private String secret;
|
||||
private String webhookSecret;
|
||||
|
||||
@Value("${deploy.app-dir}")
|
||||
private String appDir;
|
||||
@Value("${deploy.build-dir}")
|
||||
private String buildDir;
|
||||
|
||||
private Process deploymentProcess;
|
||||
private final GiteeSignatureVerifier signatureVerifier = new GiteeSignatureVerifier();
|
||||
|
||||
@PostMapping("/gitee-webhook")
|
||||
public ResponseEntity<String> handleWebhook(
|
||||
@RequestHeader(value = "X-Gitee-Token", required = false) String signature,
|
||||
@RequestHeader(value = "X-Gitee-Token", required = false) String receivedSignature,
|
||||
@RequestHeader(value = "X-Gitee-Timestamp", required = false) String timestamp,
|
||||
@RequestBody String payload) {
|
||||
|
||||
// 验证签名
|
||||
if (signature == null || !signature.equals(calculateSignature(payload))) {
|
||||
return ResponseEntity.status(401).body("无效签名");
|
||||
// 0. 基本验证
|
||||
if (StrUtil.isAllNotBlank(receivedSignature, timestamp)) {
|
||||
return ResponseEntity.status(403).body("签名验证失败");
|
||||
}
|
||||
|
||||
// 安全启动部署
|
||||
// 1. 验证签名
|
||||
if (!signatureVerifier.verifySignature(receivedSignature, timestamp, payload, webhookSecret)) {
|
||||
System.out.println("签名验证失败");
|
||||
return ResponseEntity.status(403).body("签名验证失败");
|
||||
}
|
||||
|
||||
// 2. 启动部署流程
|
||||
startDeployment();
|
||||
|
||||
return ResponseEntity.ok("部署流程已启动");
|
||||
|
@ -44,7 +50,7 @@ public class DeployController {
|
|||
@GetMapping("/deployment-status")
|
||||
public ResponseEntity<String> getDeploymentStatus() {
|
||||
try {
|
||||
Path statusFile = Path.of(appDir, "deployment-status.txt");
|
||||
Path statusFile = Path.of(buildDir, "deployment-status.txt");
|
||||
if (!Files.exists(statusFile)) {
|
||||
return ResponseEntity.ok("尚未开始部署");
|
||||
}
|
||||
|
@ -60,7 +66,7 @@ public class DeployController {
|
|||
public ResponseEntity<String> getDeploymentLog(
|
||||
@RequestParam(defaultValue = "20") int lines) {
|
||||
try {
|
||||
Path logFile = Path.of(appDir, "deploy.log");
|
||||
Path logFile = Path.of(buildDir, "deploy.log");
|
||||
if (!Files.exists(logFile)) {
|
||||
return ResponseEntity.ok("无可用日志");
|
||||
}
|
||||
|
@ -85,12 +91,9 @@ public class DeployController {
|
|||
}
|
||||
|
||||
try {
|
||||
// 清理旧状态
|
||||
Files.deleteIfExists(Path.of(appDir, "deployment-status.txt"));
|
||||
|
||||
// 启动部署脚本
|
||||
ProcessBuilder builder = new ProcessBuilder("./deploy.sh");
|
||||
builder.directory(new File(appDir));
|
||||
builder.directory(new File(buildDir));
|
||||
builder.redirectErrorStream(true);
|
||||
|
||||
deploymentProcess = builder.start();
|
||||
|
@ -119,7 +122,7 @@ public class DeployController {
|
|||
} catch (Exception e) {
|
||||
System.err.println("启动部署脚本失败: " + e.getMessage());
|
||||
try {
|
||||
Files.writeString(Path.of(appDir, "deployment-status.txt"),
|
||||
Files.writeString(Path.of(buildDir, "deployment-status.txt"),
|
||||
"START_FAILED - " + e.getMessage());
|
||||
} catch (Exception ex) {
|
||||
// 忽略
|
||||
|
@ -142,16 +145,4 @@ public class DeployController {
|
|||
}
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
private String calculateSignature(String payload) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] signatureBytes = digest.digest(
|
||||
(payload + secret).getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
return Hex.encodeHexString(signatureBytes);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("签名生成失败", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package com.dite.znpt.web.build;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
|
||||
public class GiteeSignatureVerifier {
|
||||
|
||||
private static final long MAX_TIME_DIFF = 3600 * 1000; // 1小时(毫秒)
|
||||
|
||||
public boolean verifySignature(String receivedSignature,
|
||||
String receivedTimestamp,
|
||||
String payload,
|
||||
String secret) {
|
||||
// 1. 验证时间有效性(避免重放攻击)
|
||||
if (!validateTimestamp(receivedTimestamp)) {
|
||||
System.out.println("时间戳超出允许范围");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 计算期望签名
|
||||
String computedSignature = computeSignature(
|
||||
receivedTimestamp,
|
||||
payload,
|
||||
secret
|
||||
);
|
||||
|
||||
// 3. 安全比较签名(防止时序攻击)
|
||||
return constantTimeEquals(receivedSignature, computedSignature);
|
||||
}
|
||||
|
||||
private boolean validateTimestamp(String timestampStr) {
|
||||
try {
|
||||
long timestamp = Long.parseLong(timestampStr);
|
||||
long currentTime = Instant.now().toEpochMilli();
|
||||
return Math.abs(currentTime - timestamp) < MAX_TIME_DIFF;
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("无效的时间戳格式: " + timestampStr);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String computeSignature(String timestamp, String payload, String secret) {
|
||||
try {
|
||||
// Step 1: HMAC-SHA256(ts + "\n" + secret)
|
||||
String data = timestamp + "\n" + secret;
|
||||
Mac hmac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec keySpec = new SecretKeySpec(
|
||||
secret.getBytes(StandardCharsets.UTF_8),
|
||||
"HmacSHA256"
|
||||
);
|
||||
hmac.init(keySpec);
|
||||
byte[] rawSignature = hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Step 2: Base64 encode
|
||||
String base64Sig = Base64.getEncoder().encodeToString(rawSignature);
|
||||
|
||||
// Step 3: URL encode
|
||||
return URLEncoder.encode(base64Sig, StandardCharsets.UTF_8.toString());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("签名计算失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 防止时序攻击的安全字符串比较
|
||||
private boolean constantTimeEquals(String a, String b) {
|
||||
if (a == null || b == null || a.length() != b.length()) {
|
||||
return false;
|
||||
}
|
||||
int result = 0;
|
||||
for (int i = 0; i < a.length(); i++) {
|
||||
result |= a.charAt(i) ^ b.charAt(i);
|
||||
}
|
||||
return result == 0;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue