所有文章
embedded Tomcat 上的 Spring Boot RestController 通过本地隧道接收原始 byte[] payload 并验证 webhook 签名头。
Spring BootJavawebhook debugginglocal testing

Spring Boot Webhook:原始 body、CSRF 豁免与签名头

Spring Boot 通过 @RestController 可以轻松创建 webhook 端点,但 Spring Security CSRF 和 JSON 转换器可能在 handler 运行前阻止或修改 payload。本地测试应验证 byte[] 原始访问、webhook 路径的 CSRF 豁免以及对未改动字节的签名验证。

创建 @RestController webhook 端点

保持 webhook 控制器精简。接收原始字节、验证签名后再反序列化:

@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[] 保留 Tomcat 收到的精确 payload — 适合提供商对原始 JSON 签名的场景。

通过 HttpServletRequest 读取原始 body

或者直接从 servlet 请求读取:

byte[] body = request.getInputStream().readAllBytes();
String sig = request.getHeader("Stripe-Signature");

不要先绑定到 POJO 或 Map — Jackson 重新序列化会破坏 HMAC。参见 签名验证指南

仅对 webhook 路径禁用 CSRF

Spring Security 默认启用 CSRF。没有 server-to-server 例外时 webhook POST 会返回 403:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/**"));
        return http.build();
    }
}

将 ignore 限制为 /webhooks/** — 切勿全局禁用 CSRF。

在业务逻辑前验证签名头

读取提供商请求头,从原始 byte 数组计算 digest,使用恒定时间比较。失败返回 401,成功快速返回 200 以减少重试。

本地隧道工作流

  1. 启动:./mvnw spring-boot:run 端口 8080。
  2. 暴露 Tomcat:npx portpreview 8080
  3. 将隧道 URL + 路径粘贴到控制台。
  4. 发送测试事件并查看日志。
  5. 重放相同 event ID 确认幂等 — 见 重试模式

常见陷阱

过滤器顺序与 body 消耗

在控制器之前读取 stream 的过滤器会留下空流。如需多层检查 body,使用 ContentCachingRequestWrapper

全局 @RequestBody 绑定

过早解析 JSON 的 @ControllerAdvice 可能改变字节。用 byte[] 隔离 webhook 端点。

慢 handler 触发重试

快速确认、异步处理、按 event ID 去重。

延伸阅读

基础阅读localhost 隧道基础本地 webhook 调试。签名机制见签名验证指南。防重复处理见重试与幂等性模式免费开始使用 PortPreview

常见问题

为什么 Spring Boot webhook 返回 403?
Spring Security CSRF 会阻止没有 session token 的 POST。为 webhook 路径添加 csrf.ignoringRequestMatchers,然后验证签名。
webhook 该用 byte[] 还是 POJO?
签名验证用 byte[]。POJO 绑定会重新序列化 JSON,破坏 HMAC。
如何在本地测试 Spring Boot webhook?
在 8080 运行 spring-boot:run,用 npx portpreview 暴露,将隧道 URL 粘贴到控制台并触发测试事件。