本地仓库上传

This commit is contained in:
HP 2025-03-06 01:11:16 +08:00
commit d60b4c03b1
14 changed files with 459 additions and 0 deletions

53
.gitignore vendored Normal file
View File

@ -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

77
README.md Normal file
View File

@ -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. 实现监控和告警机制

59
publisher/pom.xml Normal file
View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>webhook-publisher</artifactId>
<version>1.0.0</version>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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<WebhookSubscription> subscribe(@RequestBody WebhookSubscription subscription) {
webhookService.registerSubscription(subscription);
return ResponseEntity.ok(subscription);
}
@PostMapping("/publish")
public ResponseEntity<Void> publishEvent(@RequestBody Event event) {
webhookService.publishEvent(event);
return ResponseEntity.accepted().build();
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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<WebhookSubscription, Long> {
List<WebhookSubscription> findByActiveTrue();
}

View File

@ -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<WebhookSubscription> 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<Event> 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 "";
}
}
}

View File

@ -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

50
subscriber/pom.xml Normal file
View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>webhook-subscriber</artifactId>
<version>1.0.0</version>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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<Void> receiveWebhook(
@RequestHeader("X-Webhook-Signature") String signature,
@RequestHeader("X-Event-Type") String eventType,
@RequestBody Map<String, Object> 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();
}
}

View File

@ -0,0 +1,6 @@
server:
port: 8081
logging:
level:
com.example.webhook: DEBUG