すべての記事
embedded Tomcat 上の Spring Boot RestController がローカルトンネル経由の raw byte[] payload から webhook 署名ヘッダーを検証。
Spring BootJavawebhook debugginglocal testing

Spring Boot Webhook:raw body、CSRF 例外、署名ヘッダー

Spring Boot は @RestController で webhook エンドポイントを簡単に作れますが、Spring Security CSRF と JSON コンバーターが handler 前に payload をブロックまたは変更することがあります。ローカルテストでは byte[] の raw アクセス、webhook パスの CSRF 例外、未変更バイトでの署名検証を確認してください。

@RestController webhook エンドポイントを作成

webhook コントローラーは薄く保ちます。raw バイトを受け取り、署名を検証してからデシリアライズ:

@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 を保持 — プロバイダーが raw JSON に署名する場合に最適。

HttpServletRequest で raw 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 をグローバルに無効化しない。

ビジネスロジック前に署名ヘッダーを検証

プロバイダーヘッダーを読み、raw byte 配列から digest を計算し、定数時間で比較。不一致は 401、成功は迅速に 200。

ローカルトンネルワークフロー

  1. 起動: ./mvnw spring-boot:run ポート 8080。
  2. Tomcat を公開: npx portpreview 8080
  3. トンネル URL + パスをダッシュボードに貼付。
  4. テストイベントを送信してログを確認。
  5. 同じ event ID を再生して冪等性を確認 — リトライパターン

よくある落とし穴

フィルター順序と body 消費

コントローラー前に stream を読むフィルターは空にします。必要なら ContentCachingRequestWrapper を使用。

グローバル @RequestBody バインド

早期 JSON パースの @ControllerAdvice はバイトを変える。webhook は byte[] で分離。

遅い handler はリトライを誘発

迅速に ACK、async 処理、event ID で重複排除。

さらに学ぶ

基礎はlocalhost トンネリングローカル webhook デバッグ。署名の仕組みは署名検証ガイド。重複安全なハンドラーはリトライと冪等性のパターンPortPreview を無料で始める

よくある質問

Spring Boot webhook が 403 になるのはなぜ?
Spring Security CSRF がセッショントークンなし POST をブロックします。webhook パスに csrf.ignoringRequestMatchers を追加し、署名を検証してください。
webhook には byte[] と POJO どちら?
署名検証には byte[] を使います。POJO バインドは JSON を再シリアライズし HMAC を壊します。
Spring Boot webhook をローカルでテストするには?
8080 で spring-boot:run を実行し、npx portpreview で公開、トンネル URL を貼り、テストイベントを送信します。