Compare commits
2 Commits
45ae5d964a
...
2b66290884
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b66290884 | |||
| d60b4c03b1 |
53
.gitignore
vendored
Normal file
53
.gitignore
vendored
Normal 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
|
||||||
80
README.md
80
README.md
@ -1,2 +1,80 @@
|
|||||||
# webhook-prac
|
|
||||||
|
# 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
59
publisher/pom.xml
Normal 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>
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
19
publisher/src/main/java/com/example/webhook/model/Event.java
Normal file
19
publisher/src/main/java/com/example/webhook/model/Event.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
publisher/src/main/resources/application.yml
Normal file
18
publisher/src/main/resources/application.yml
Normal 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
50
subscriber/pom.xml
Normal 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>
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
6
subscriber/src/main/resources/application.yml
Normal file
6
subscriber/src/main/resources/application.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
server:
|
||||||
|
port: 8081
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.example.webhook: DEBUG
|
||||||
Loading…
x
Reference in New Issue
Block a user