Spring Boot cookbook · Intermediate
Verify webhook signatures in Spring Boot
Accept signed webhooks from a provider, reject forgeries and replays, and process each event once.
Approach
Receive the raw body as a String/byte[], compute an HMAC-SHA256 over timestamp.body, compare in constant time with MessageDigest.isEqual, check the timestamp tolerance, then insert the event ID with a unique constraint and process asynchronously.
Solution
@RestController
class PaymentWebhookController {
private final byte[] secret;
private final WebhookEventRepository events;
private final ObjectMapper json;
PaymentWebhookController(@Value("${payments.webhook-secret}") String secret, WebhookEventRepository events, ObjectMapper json) {
this.secret = secret.getBytes(StandardCharsets.UTF_8); this.events = events; this.json = json;
}
@PostMapping(path = "/webhooks/payments", consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<Void> receive(@RequestHeader("X-Signature") String signature, @RequestBody String body) throws Exception {
Map<String, String> parts = Arrays.stream(signature.split(","))
.map(p -> p.split("=", 2)).filter(a -> a.length == 2)
.collect(Collectors.toMap(a -> a[0], a -> a[1]));
long ts = Long.parseLong(parts.getOrDefault("t", "0"));
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret, "HmacSHA256"));
byte[] expected = HexFormat.of().formatHex(mac.doFinal((ts + "." + body).getBytes(StandardCharsets.UTF_8)))
.getBytes(StandardCharsets.UTF_8);
byte[] given = parts.getOrDefault("v1", "").getBytes(StandardCharsets.UTF_8);
if (Math.abs(Instant.now().getEpochSecond() - ts) > 300 || !MessageDigest.isEqual(expected, given)) {
return ResponseEntity.badRequest().build();
}
JsonNode event = json.readTree(body);
events.insertIfAbsent(event.get("id").asText(), event.get("type").asText(), body); // ON CONFLICT DO NOTHING
return ResponseEntity.ok().build();
}
}
Pitfalls
- Binding the body to a DTO first and verifying re-serialized JSON.
String.equalsfor signature comparison.- Doing slow work before responding.
Variations
- Use the provider's Java SDK verifier (e.g., Stripe
Webhook.constructEvent).
Further reading: Stripe: Verify webhook events
Reference: https://docs.stripe.com/webhooks#verify-events
More Spring Boot recipes
- Add correlation IDs to Spring Boot logs
- Add retries and a circuit breaker with Resilience4j
- Authenticate machine clients with API keys in Spring Security
- Cache service results in Redis with @Cacheable
- Consume Kafka messages with retries and a dead-letter topic
- Handle concurrent updates with optimistic locking
- Implement soft delete with Hibernate
- Make POST endpoints idempotent in Spring Boot