diff --git a/web/src/main/java/com/dite/znpt/web/controller/DeployController.java b/web/src/main/java/com/dite/znpt/web/build/DeployController.java similarity index 73% rename from web/src/main/java/com/dite/znpt/web/controller/DeployController.java rename to web/src/main/java/com/dite/znpt/web/build/DeployController.java index c3045e7..83de059 100644 --- a/web/src/main/java/com/dite/znpt/web/controller/DeployController.java +++ b/web/src/main/java/com/dite/znpt/web/build/DeployController.java @@ -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 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 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 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); - } - } } diff --git a/web/src/main/java/com/dite/znpt/web/build/GiteeSignatureVerifier.java b/web/src/main/java/com/dite/znpt/web/build/GiteeSignatureVerifier.java new file mode 100644 index 0000000..0da970d --- /dev/null +++ b/web/src/main/java/com/dite/znpt/web/build/GiteeSignatureVerifier.java @@ -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; + } +}