Spring Boot ทำ webhook endpoint ด้วย @RestController ได้ง่าย แต่ Spring Security CSRF และ JSON converter อาจบล็อกหรือเปลี่ยน payload ก่อน handler ทดสอบในเครื่องต้องพิสูจน์การเข้าถึง byte[] ดิบ ยกเว้น CSRF บน path webhook และตรวจลายเซ็นบน byte ที่ไม่ถูกแตะ
สร้าง webhook endpoint @RestController
เก็บ controller ให้บาง รับ byte ดิบ ตรวจลายเซ็น แล้ว deserialize:
@RestController
@RequestMapping("/webhooks")
public class StripeWebhookController {
@PostMapping("/stripe")
public ResponseEntity handle(
@RequestBody byte[] payload,
@RequestHeader("Stripe-Signature") String signature) {
if (!verifier.isValid(payload, signature)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
JsonNode event = objectMapper.readTree(payload);
handler.process(event);
return ResponseEntity.ok().build();
}
}
@RequestBody byte[] เก็บ payload ตรงที่ Tomcat รับ — เหมาะเมื่อ provider เซ็น JSON ดิบ
อ่าน raw body ด้วย HttpServletRequest
ทางเลือก: อ่านจาก servlet request โดยตรง:
byte[] body = request.getInputStream().readAllBytes();
String sig = request.getHeader("Stripe-Signature");
อย่า bind POJO หรือ Map ก่อน — Jackson reserialize ทำให้ HMAC พัง ดู คู่มือตรวจลายเซ็น
ปิด CSRF เฉพาะ path webhook
Spring Security เปิด CSRF โดยค่าเริ่มต้น POST webhook ล้ม 403 หากไม่มีข้อยกเว้น server-to-server:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/**"));
return http.build();
}
}
จำกัด ignore ที่ /webhooks/** — อย่าปิด CSRF ทั้งแอป
ตรวจ header ลายเซ็นก่อน business logic
อ่าน header จาก provider คำนวณ digest จาก byte array เปรียบเทียบแบบ constant-time 401 ถ้าผิด 200 เร็วเมื่อสำเร็จ
ขั้นตอน tunnel ในเครื่อง
- เริ่ม:
./mvnw spring-boot:runพอร์ต 8080 - เปิด Tomcat:
npx portpreview 8080 - วาง URL tunnel + path ใน dashboard
- ส่ง event ทดสอบและดู log
- เล่น event ID เดิมเพื่อ idempotency — ดู รูปแบบ retry
กับดักที่พบบ่อย
ลำดับ filter และการ consume body
filter ที่อ่าน stream ก่อน controller ทำให้ stream ว่าง ใช้ ContentCachingRequestWrapper หากจำเป็น
Global @RequestBody binding
@ControllerAdvice ที่ parse JSON เร็วอาจเปลี่ยน byte แยก webhook ด้วย byte[]
Handler ช้ากระตุ้น retry
ตอบเร็ว ประมวลผล async dedupe ด้วย event ID
อ่านเพิ่ม
พื้นฐานอ่านที่ พื้นฐาน localhost tunneling และ การดีบัก webhook ในเครื่อง. กลไกลายเซ็นอ่านที่ คู่มือตรวจสอบลายเซ็น. handler ป้องกันซ้ำอ่านที่ รูปแบบ retry และ idempotency. เริ่มใช้ PortPreview ฟรี.
![Spring Boot RestController บน embedded Tomcat ตรวจ header ลายเซ็น webhook จาก byte[] ดิบที่รับผ่าน tunnel ในเครื่อง](/_next/image?url=%2Fimages%2Farticles%2Fspring-boot-webhook-local-testing%2Fcover.png&w=3840&q=75)