1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spring Boot 3におけるCSRFエラーの環境依存問題

1
Last updated at Posted at 2026-01-29

Spring Boot 3におけるCSRFエラーの環境依存問題

概要

Spring Boot 3 (Spring Security 6.x) 環境で、端末やデプロイ環境によってCSRFエラーが発生したりしなかったりする現象について、原因と対策をまとめます。


1. Spring Boot 3 / Spring Security 6の主な変更点

1.1 デフォルト設定の変更

Spring Security 6.xでは、セキュリティ設定がより厳格になっています:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCSRfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
            )
            // ...
        return http.build();
    }
}

重要: Spring Boot 3ではLambda DSL形式が推奨され、従来のメソッドチェーン形式は非推奨となりました。


2. 環境依存でCSRFエラーが発生する主な原因

2.1 Cookie設定の違い

原因

# application.yml
server:
  servlet:
    session:
      cookie:
        secure: true      # HTTPSでのみ送信
        same-site: strict # Same-Site属性
        domain: .example.com # Cookie有効ドメイン

環境による違い:

環境 HTTPS Cookie送信 CSRF動作
ローカル開発 (localhost) ❌ HTTP ⚠️ Secure=trueだと送信されない ❌ エラー
ステージング環境 ✅ HTTPS ✅ 送信される ✅ 正常
本番環境 ✅ HTTPS ✅ 送信される ✅ 正常

解決策

環境別にprofileを設定:

# application-local.yml
server:
  servlet:
    session:
      cookie:
        secure: false
        same-site: lax

# application-prod.yml
server:
  servlet:
    session:
      cookie:
        secure: true
        same-site: strict

2.2 SameSite属性の問題

原因

// CookieのSameSite属性設定
@Bean
public CookieSameSiteSupplier cookieSameSiteSupplier() {
    return CookieSameSiteSupplier.ofStrict();
}

ブラウザによる挙動の違い:

  • Chrome/Edge: SameSite=Lax がデフォルト
  • Safari: より厳格な制限
  • Firefox: 設定により動作が異なる

クロスオリジンリクエストの場合、SameSite=Strictではクッキーが送信されません。

解決策

server:
  servlet:
    session:
      cookie:
        same-site: lax  # または none (HTTPS必須)

2.3 リバースプロキシ/ロードバランサー経由の問題

原因

[ブラウザ] --HTTPS--> [ALB/Nginx] --HTTP--> [Spring Boot]

Spring Bootアプリケーションが自身をHTTPと認識し、HTTPS用のCookie設定が機能しない。

解決策

方法1: ForwardedHeaderFilterの有効化

server:
  forward-headers-strategy: framework

または

@Bean
public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
    FilterRegistrationBean<ForwardedHeaderFilter> bean = new FilterRegistrationBean<>();
    bean.setFilter(new ForwardedHeaderFilter());
    return bean;
}

方法2: TomcatのリモートIPバルブ設定

@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
    return factory -> factory.addContextCustomizers(context -> {
        RemoteIpValve valve = new RemoteIpValve();
        valve.setRemoteIpHeader("X-Forwarded-For");
        valve.setProtocolHeader("X-Forwarded-Proto");
        context.getPipeline().addValve(valve);
    });
}

2.4 CORS設定の問題

原因

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://localhost:3000")  // 特定環境のみ許可
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowCredentials(true);
    }
}

開発環境と本番環境でオリジンが異なる場合、CSRF Cookieが送信されない。

解決策

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Value("${app.cors.allowed-origins}")
    private String[] allowedOrigins;
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins(allowedOrigins)
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowCredentials(true)
            .allowedHeaders("*")
            .exposedHeaders("X-CSRF-TOKEN");
    }
}
# application-local.yml
app:
  cors:
    allowed-origins: http://localhost:3000,http://localhost:8080

# application-prod.yml
app:
  cors:
    allowed-origins: https://example.com

2.5 CSRF Token取得方法の問題

原因

Spring Security 6.xでは、CSRF Tokenの取得方法が変更されました。

古い方法 (動作しない場合あり):

// ヘッダーから直接取得
fetch('/api/data', {
    headers: {
        'X-CSRF-TOKEN': document.querySelector('meta[name="_csrf"]').content
    }
})

新しい方法:

// Cookieから取得
function getCsrfToken() {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; XSRF-TOKEN=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

fetch('/api/data', {
    method: 'POST',
    headers: {
        'X-XSRF-TOKEN': getCsrfToken()
    },
    credentials: 'include'
})

解決策 (Spring側設定)

@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
        );
        return http.build();
    }
}

2.6 セッション管理の違い

原因

spring:
  session:
    store-type: none  # ローカル: メモリ
    # store-type: redis  # 本番: Redis

セッション永続化方法が異なると、CSRFトークンの保持方法も変わります。

解決策

開発環境と本番環境で同じセッション管理を使用:

# 全環境共通
spring:
  session:
    store-type: jdbc
    jdbc:
      initialize-schema: always

2.7 プロキシ・VPN環境の問題

原因

企業のプロキシやVPN経由では:

  • IPアドレスが変わる
  • HTTPヘッダーが書き換えられる
  • Cookieが破棄される場合がある

解決策

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            .requireCsrfProtectionMatcher(new RequestMatcher() {
                private final Pattern allowedMethods = 
                    Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
                
                @Override
                public boolean matches(HttpServletRequest request) {
                    // プロキシ経由の場合の特別処理
                    String forwarded = request.getHeader("X-Forwarded-For");
                    if (forwarded != null && forwarded.contains("proxy-ip")) {
                        return !allowedMethods.matcher(request.getMethod()).matches();
                    }
                    return !allowedMethods.matcher(request.getMethod()).matches();
                }
            })
        );
    return http.build();
}

3. デバッグ方法

3.1 ログ設定

logging:
  level:
    org.springframework.security: DEBUG
    org.springframework.web.csrf: TRACE

3.2 カスタムフィルターでデバッグ

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CsrfDebugFilter extends OncePerRequestFilter {
    
    private static final Logger logger = LoggerFactory.getLogger(CsrfDebugFilter.class);
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        
        logger.debug("=== CSRF Debug Info ===");
        logger.debug("Request URI: {}", request.getRequestURI());
        logger.debug("Method: {}", request.getMethod());
        logger.debug("Secure: {}", request.isSecure());
        logger.debug("Protocol: {}", request.getProtocol());
        
        // ヘッダー情報
        logger.debug("X-Forwarded-Proto: {}", request.getHeader("X-Forwarded-Proto"));
        logger.debug("X-Forwarded-For: {}", request.getHeader("X-Forwarded-For"));
        logger.debug("X-CSRF-TOKEN: {}", request.getHeader("X-CSRF-TOKEN"));
        logger.debug("X-XSRF-TOKEN: {}", request.getHeader("X-XSRF-TOKEN"));
        
        // Cookie情報
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                logger.debug("Cookie: {} = {}", cookie.getName(), cookie.getValue());
            }
        }
        
        filterChain.doFilter(request, response);
    }
}

4. 推奨設定パターン

4.1 基本設定 (本番環境)

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            );
        
        return http.build();
    }
}

final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
    private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, 
                       Supplier<CsrfToken> csrfToken) {
        this.delegate.handle(request, response, csrfToken);
    }
}

4.2 application.yml (環境別)

# application.yml (共通)
server:
  servlet:
    session:
      timeout: 30m
      cookie:
        http-only: true
        path: /

---
# application-local.yml
server:
  servlet:
    session:
      cookie:
        secure: false
        same-site: lax

---
# application-prod.yml
server:
  servlet:
    session:
      cookie:
        secure: true
        same-site: strict
        domain: .example.com
  forward-headers-strategy: framework

5. チェックリスト

環境間の差異を確認する際のチェックリスト:

  • HTTP vs HTTPS の違い
  • Cookie の Secure 属性設定
  • Cookie の SameSite 属性設定
  • リバースプロキシ/ロードバランサーの有無
  • X-Forwarded-* ヘッダーの処理
  • CORS 設定の allowed-origins
  • ブラウザのバージョンと設定
  • プロキシ/VPN 環境の有無
  • セッション管理方法の違い
  • CSRF Token の取得・送信方法

6. まとめ

Spring Boot 3でCSRFエラーが環境依存で発生する主な原因:

  1. Cookie設定の不一致 - Secure属性とHTTP/HTTPSの組み合わせ
  2. SameSite属性の制限 - クロスオリジンリクエストでのCookie送信
  3. プロキシ環境の違い - X-Forwarded-*ヘッダーの処理不足
  4. CORS設定の環境差異 - allowed-originsの設定ミス
  5. セッション管理の違い - 永続化方法の不統一

推奨対策:

  • 環境別のSpring Profileを活用
  • ForwardedHeaderFilterを有効化
  • 統一されたCSRF Token取得・送信フロー
  • 詳細なデバッグログの設定


1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?