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エラーが環境依存で発生する主な原因:
- Cookie設定の不一致 - Secure属性とHTTP/HTTPSの組み合わせ
- SameSite属性の制限 - クロスオリジンリクエストでのCookie送信
- プロキシ環境の違い - X-Forwarded-*ヘッダーの処理不足
- CORS設定の環境差異 - allowed-originsの設定ミス
- セッション管理の違い - 永続化方法の不統一
推奨対策:
- 環境別のSpring Profileを活用
- ForwardedHeaderFilterを有効化
- 統一されたCSRF Token取得・送信フロー
- 詳細なデバッグログの設定