From d60b4c03b14577ffc55610a3f2f7ab5a2e9b6474 Mon Sep 17 00:00:00 2001 From: HP Date: Thu, 6 Mar 2025 01:11:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E4=BB=93=E5=BA=93=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 53 +++++++++++++ README.md | 77 +++++++++++++++++++ publisher/pom.xml | 59 ++++++++++++++ .../example/webhook/PublisherApplication.java | 13 ++++ .../webhook/controller/WebhookController.java | 28 +++++++ .../java/com/example/webhook/model/Event.java | 19 +++++ .../webhook/model/WebhookSubscription.java | 20 +++++ .../WebhookSubscriptionRepository.java | 9 +++ .../webhook/service/WebhookService.java | 65 ++++++++++++++++ publisher/src/main/resources/application.yml | 18 +++++ subscriber/pom.xml | 50 ++++++++++++ .../webhook/SubscriberApplication.java | 11 +++ .../controller/WebhookReceiverController.java | 31 ++++++++ subscriber/src/main/resources/application.yml | 6 ++ 14 files changed, 459 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 publisher/pom.xml create mode 100644 publisher/src/main/java/com/example/webhook/PublisherApplication.java create mode 100644 publisher/src/main/java/com/example/webhook/controller/WebhookController.java create mode 100644 publisher/src/main/java/com/example/webhook/model/Event.java create mode 100644 publisher/src/main/java/com/example/webhook/model/WebhookSubscription.java create mode 100644 publisher/src/main/java/com/example/webhook/repository/WebhookSubscriptionRepository.java create mode 100644 publisher/src/main/java/com/example/webhook/service/WebhookService.java create mode 100644 publisher/src/main/resources/application.yml create mode 100644 subscriber/pom.xml create mode 100644 subscriber/src/main/java/com/example/webhook/SubscriberApplication.java create mode 100644 subscriber/src/main/java/com/example/webhook/controller/WebhookReceiverController.java create mode 100644 subscriber/src/main/resources/application.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99103a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Application Specific ### +application-dev.yml +application-prod.yml +*.log +logs/ +*.properties + +### System Files ### +.DS_Store +Thumbs.db + +### Maven ### +.mvn/ +mvnw +mvnw.cmd + +### Configuration Files ### +src/main/resources/application-*.yml +!src/main/resources/application.yml \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3698806 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# WebHook 示例项目 + +这个项目展示了 WebHook 的基本概念和实现方式,包含两个服务: +- Publisher:发布事件并向订阅者发送 WebHook 通知 +- Subscriber:接收和处理 WebHook 通知 + +## 功能特点 + +1. WebHook 订阅管理 +2. 异步事件发布 +3. 签名验证机制 +4. 失败重试机制 +5. 事件类型支持 + +## 项目结构 + +``` +webhook-example/ +├── publisher/ # WebHook 发布者服务 +├── subscriber/ # WebHook 接收者服务 +``` + +## 运行说明 + +1. 启动 Publisher 服务(端口 8080) +```bash +cd publisher +mvn spring-boot:run +``` + +2. 启动 Subscriber 服务(端口 8081) +```bash +cd subscriber +mvn spring-boot:run +``` + +## API 使用示例 + +1. 注册 WebHook 订阅: +```bash +curl -X POST http://localhost:8080/api/webhooks/subscribe \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Test Webhook", + "url": "http://localhost:8081/api/webhook/receive", + "secret": "your-secret-key" + }' +``` + +2. 发布事件: +```bash +curl -X POST http://localhost:8080/api/webhooks/publish \ + -H "Content-Type: application/json" \ + -d '{ + "type": "user.created", + "description": "New user registration", + "data": { + "userId": "123", + "email": "test@example.com" + } + }' +``` + +## 安全性考虑 + +1. 使用 HTTPS 进行通信 +2. 实现签名验证机制 +3. 使用 Secret Key 保护 WebHook 端点 +4. 实现速率限制 + +## 最佳实践 + +1. 实现幂等性处理 +2. 添加重试机制 +3. 设置超时限制 +4. 记录详细的日志 +5. 实现监控和告警机制 \ No newline at end of file diff --git a/publisher/pom.xml b/publisher/pom.xml new file mode 100644 index 0000000..8bc4a25 --- /dev/null +++ b/publisher/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.0 + + + + com.example + webhook-publisher + 1.0.0 + + + 11 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + runtime + + + org.projectlombok + lombok + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + \ No newline at end of file diff --git a/publisher/src/main/java/com/example/webhook/PublisherApplication.java b/publisher/src/main/java/com/example/webhook/PublisherApplication.java new file mode 100644 index 0000000..9fa5f4a --- /dev/null +++ b/publisher/src/main/java/com/example/webhook/PublisherApplication.java @@ -0,0 +1,13 @@ +package com.example.webhook; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; + +@SpringBootApplication +@EnableAsync +public class PublisherApplication { + public static void main(String[] args) { + SpringApplication.run(PublisherApplication.class, args); + } +} \ No newline at end of file diff --git a/publisher/src/main/java/com/example/webhook/controller/WebhookController.java b/publisher/src/main/java/com/example/webhook/controller/WebhookController.java new file mode 100644 index 0000000..7df6690 --- /dev/null +++ b/publisher/src/main/java/com/example/webhook/controller/WebhookController.java @@ -0,0 +1,28 @@ +package com.example.webhook.controller; + +import com.example.webhook.model.Event; +import com.example.webhook.model.WebhookSubscription; +import com.example.webhook.service.WebhookService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/webhooks") +@RequiredArgsConstructor +public class WebhookController { + + private final WebhookService webhookService; + + @PostMapping("/subscribe") + public ResponseEntity subscribe(@RequestBody WebhookSubscription subscription) { + webhookService.registerSubscription(subscription); + return ResponseEntity.ok(subscription); + } + + @PostMapping("/publish") + public ResponseEntity publishEvent(@RequestBody Event event) { + webhookService.publishEvent(event); + return ResponseEntity.accepted().build(); + } +} \ No newline at end of file diff --git a/publisher/src/main/java/com/example/webhook/model/Event.java b/publisher/src/main/java/com/example/webhook/model/Event.java new file mode 100644 index 0000000..12ecd45 --- /dev/null +++ b/publisher/src/main/java/com/example/webhook/model/Event.java @@ -0,0 +1,19 @@ +package com.example.webhook.model; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class Event { + private String type; + private String description; + private LocalDateTime timestamp; + private Object data; + + public Event(String type, String description, Object data) { + this.type = type; + this.description = description; + this.data = data; + this.timestamp = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/publisher/src/main/java/com/example/webhook/model/WebhookSubscription.java b/publisher/src/main/java/com/example/webhook/model/WebhookSubscription.java new file mode 100644 index 0000000..075b4b8 --- /dev/null +++ b/publisher/src/main/java/com/example/webhook/model/WebhookSubscription.java @@ -0,0 +1,20 @@ +package com.example.webhook.model; + +import lombok.Data; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +@Data +public class WebhookSubscription { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private String url; + private String secret; + private boolean active = true; +} \ No newline at end of file diff --git a/publisher/src/main/java/com/example/webhook/repository/WebhookSubscriptionRepository.java b/publisher/src/main/java/com/example/webhook/repository/WebhookSubscriptionRepository.java new file mode 100644 index 0000000..71fbb8a --- /dev/null +++ b/publisher/src/main/java/com/example/webhook/repository/WebhookSubscriptionRepository.java @@ -0,0 +1,9 @@ +package com.example.webhook.repository; + +import com.example.webhook.model.WebhookSubscription; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface WebhookSubscriptionRepository extends JpaRepository { + List findByActiveTrue(); +} \ No newline at end of file diff --git a/publisher/src/main/java/com/example/webhook/service/WebhookService.java b/publisher/src/main/java/com/example/webhook/service/WebhookService.java new file mode 100644 index 0000000..3131cf2 --- /dev/null +++ b/publisher/src/main/java/com/example/webhook/service/WebhookService.java @@ -0,0 +1,65 @@ +package com.example.webhook.service; + +import com.example.webhook.model.Event; +import com.example.webhook.model.WebhookSubscription; +import com.example.webhook.repository.WebhookSubscriptionRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class WebhookService { + + private final WebhookSubscriptionRepository subscriptionRepository; + private final RestTemplate restTemplate = new RestTemplate(); + + public void registerSubscription(WebhookSubscription subscription) { + subscriptionRepository.save(subscription); + } + + @Async + public void publishEvent(Event event) { + List activeSubscriptions = subscriptionRepository.findByActiveTrue(); + + for (WebhookSubscription subscription : activeSubscriptions) { + try { + String signature = generateSignature(event, subscription.getSecret()); + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Webhook-Signature", signature); + headers.set("X-Event-Type", event.getType()); + + HttpEntity request = new HttpEntity<>(event, headers); + restTemplate.postForEntity(subscription.getUrl(), request, String.class); + + log.info("Successfully sent webhook to {}", subscription.getUrl()); + } catch (Exception e) { + log.error("Failed to send webhook to {}: {}", subscription.getUrl(), e.getMessage()); + } + } + } + + private String generateSignature(Event event, String secret) { + try { + Mac sha256Hmac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256"); + sha256Hmac.init(secretKey); + String payload = event.getTimestamp().toString() + event.getType(); + byte[] hash = sha256Hmac.doFinal(payload.getBytes()); + return Base64.getEncoder().encodeToString(hash); + } catch (Exception e) { + log.error("Failed to generate signature: {}", e.getMessage()); + return ""; + } + } +} \ No newline at end of file diff --git a/publisher/src/main/resources/application.yml b/publisher/src/main/resources/application.yml new file mode 100644 index 0000000..74cc4c2 --- /dev/null +++ b/publisher/src/main/resources/application.yml @@ -0,0 +1,18 @@ +server: + port: 8080 + +spring: + datasource: + url: jdbc:h2:mem:webhookdb + driver-class-name: org.h2.Driver + username: sa + password: + h2: + console: + enabled: true + path: /h2-console + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: update + show-sql: true \ No newline at end of file diff --git a/subscriber/pom.xml b/subscriber/pom.xml new file mode 100644 index 0000000..7056235 --- /dev/null +++ b/subscriber/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.0 + + + + com.example + webhook-subscriber + 1.0.0 + + + 11 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + \ No newline at end of file diff --git a/subscriber/src/main/java/com/example/webhook/SubscriberApplication.java b/subscriber/src/main/java/com/example/webhook/SubscriberApplication.java new file mode 100644 index 0000000..33e7955 --- /dev/null +++ b/subscriber/src/main/java/com/example/webhook/SubscriberApplication.java @@ -0,0 +1,11 @@ +package com.example.webhook; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SubscriberApplication { + public static void main(String[] args) { + SpringApplication.run(SubscriberApplication.class, args); + } +} \ No newline at end of file diff --git a/subscriber/src/main/java/com/example/webhook/controller/WebhookReceiverController.java b/subscriber/src/main/java/com/example/webhook/controller/WebhookReceiverController.java new file mode 100644 index 0000000..e22b1f0 --- /dev/null +++ b/subscriber/src/main/java/com/example/webhook/controller/WebhookReceiverController.java @@ -0,0 +1,31 @@ +package com.example.webhook.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/webhook") +@Slf4j +public class WebhookReceiverController { + + @PostMapping("/receive") + public ResponseEntity receiveWebhook( + @RequestHeader("X-Webhook-Signature") String signature, + @RequestHeader("X-Event-Type") String eventType, + @RequestBody Map payload + ) { + log.info("Received webhook event of type: {}", eventType); + log.info("Signature: {}", signature); + log.info("Payload: {}", payload); + + // Here you would typically: + // 1. Verify the signature + // 2. Process the event based on its type + // 3. Perform relevant business logic + + return ResponseEntity.ok().build(); + } +} \ No newline at end of file diff --git a/subscriber/src/main/resources/application.yml b/subscriber/src/main/resources/application.yml new file mode 100644 index 0000000..e3523b8 --- /dev/null +++ b/subscriber/src/main/resources/application.yml @@ -0,0 +1,6 @@ +server: + port: 8081 + +logging: + level: + com.example.webhook: DEBUG \ No newline at end of file