Spring Securityで認証・認可の問題が発生した際、適切なデバッグ手法を知らないと原因特定に時間がかかります。本記事では、デバッグログの有効化方法、カスタムエラーハンドラの実装、そしてよくある設定ミスとその対処法を実践的に解説します。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Java 17以上
Spring Boot 3.4.x
Spring Security 6.4.x
ビルドツール Maven または Gradle
IDE VS Code または IntelliJ IDEA

事前に以下の知識があると理解がスムーズです。

  • Spring Securityの基本的な設定方法
  • SecurityFilterChainの仕組み
  • ログ設定(Logback)の基礎

期待される学習成果

本記事を読み終えると、以下のことができるようになります。

  • Spring Securityのデバッグログを適切に設定し、認証・認可の処理フローを追跡できる
  • AuthenticationEntryPointとAccessDeniedHandlerをカスタマイズして、詳細なエラー情報を取得できる
  • よくある設定ミスのパターンを理解し、迅速に問題を解決できる

デバッグログの有効化

Spring Securityは認証・認可の処理において詳細なログを出力する機能を備えています。問題発生時には、まずこのログを有効化して原因を特定します。

基本的なログレベル設定

application.propertiesまたはapplication.ymlでSpring Securityのログレベルを設定します。

1
2
# application.properties
logging.level.org.springframework.security=DEBUG
1
2
3
4
# application.yml
logging:
  level:
    org.springframework.security: DEBUG

DEBUGレベルを有効化すると、以下のような情報が出力されます。

  • SecurityFilterChainに登録されたフィルター一覧
  • 各フィルターの実行状況
  • 認証処理の結果
  • 認可判定の詳細

より詳細なTRACEレベルの活用

DEBUGレベルで情報が不足する場合は、TRACEレベルを使用します。

1
2
# application.properties
logging.level.org.springframework.security=TRACE

TRACEレベルでは、以下の追加情報が出力されます。

  • 各フィルターの呼び出しタイミング
  • リクエストマッチングの詳細
  • 認証プロバイダーの選択過程

ログ出力例の解説

以下は、CSRFトークンが不正な場合のDEBUGログ出力例です。

1
2
3
4
5
6
2026-01-13T10:15:30.123+09:00 DEBUG 12345 --- [nio-8080-exec-1] o.s.s.web.FilterChainProxy        : Securing POST /api/users
2026-01-13T10:15:30.124+09:00 TRACE 12345 --- [nio-8080-exec-1] o.s.s.web.FilterChainProxy        : Invoking DisableEncodeUrlFilter (1/15)
2026-01-13T10:15:30.125+09:00 TRACE 12345 --- [nio-8080-exec-1] o.s.s.web.FilterChainProxy        : Invoking SecurityContextHolderFilter (3/15)
2026-01-13T10:15:30.126+09:00 TRACE 12345 --- [nio-8080-exec-1] o.s.s.web.FilterChainProxy        : Invoking CsrfFilter (5/15)
2026-01-13T10:15:30.130+09:00 DEBUG 12345 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter  : Invalid CSRF token found for http://localhost:8080/api/users
2026-01-13T10:15:30.131+09:00 DEBUG 12345 --- [nio-8080-exec-1] o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code

このログから、CsrfFilterでCSRFトークンの検証に失敗し、403エラーが返されていることがわかります。

パッケージ別のログ設定

特定の領域のみをデバッグしたい場合は、パッケージを絞ってログレベルを設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 認証処理のデバッグ
logging.level.org.springframework.security.authentication=DEBUG

# 認可処理のデバッグ
logging.level.org.springframework.security.authorization=DEBUG

# Web関連のセキュリティ処理
logging.level.org.springframework.security.web=DEBUG

# FilterChainの詳細
logging.level.org.springframework.security.web.FilterChainProxy=TRACE

以下に主要なパッケージとその役割を示します。

パッケージ 役割
o.s.s.authentication 認証処理(AuthenticationManager、AuthenticationProvider)
o.s.s.authorization 認可処理(AuthorizationManager)
o.s.s.web Webセキュリティ全般(Filter、Handler)
o.s.s.web.csrf CSRF保護機能
o.s.s.web.access アクセス制御(AccessDeniedHandler)

Logbackによる詳細設定

より細かなログ制御が必要な場合は、logback-spring.xmlを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

    <!-- 本番環境ではINFO、開発環境ではDEBUG -->
    <springProfile name="dev">
        <logger name="org.springframework.security" level="DEBUG" additivity="false">
            <appender-ref ref="CONSOLE"/>
        </logger>
        <logger name="org.springframework.security.web.FilterChainProxy" level="TRACE" additivity="false">
            <appender-ref ref="CONSOLE"/>
        </logger>
    </springProfile>

    <springProfile name="prod">
        <logger name="org.springframework.security" level="WARN" additivity="false">
            <appender-ref ref="CONSOLE"/>
        </logger>
    </springProfile>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

SecurityFilterChainの確認

Spring Securityでは、アプリケーション起動時にSecurityFilterChainに登録されたフィルター一覧がDEBUGレベルで出力されます。この情報は、フィルターの順序や設定の確認に役立ちます。

起動時のフィルター一覧確認

アプリケーション起動時に以下のようなログが出力されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
2026-01-13T10:00:00.000+09:00 DEBUG 12345 --- [main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
    DisableEncodeUrlFilter,
    WebAsyncManagerIntegrationFilter,
    SecurityContextHolderFilter,
    HeaderWriterFilter,
    CsrfFilter,
    LogoutFilter,
    UsernamePasswordAuthenticationFilter,
    DefaultLoginPageGeneratingFilter,
    DefaultLogoutPageGeneratingFilter,
    BasicAuthenticationFilter,
    RequestCacheAwareFilter,
    SecurityContextHolderAwareRequestFilter,
    AnonymousAuthenticationFilter,
    ExceptionTranslationFilter,
    AuthorizationFilter
]

このログにより、以下の点を確認できます。

  • カスタムフィルターが正しい位置に挿入されているか
  • 不要なフィルターが含まれていないか
  • フィルターの実行順序が意図通りか

フィルター順序の図解

Spring Securityのフィルターは特定の順序で実行されます。以下にその流れを示します。

flowchart TD
    A[HTTPリクエスト] --> B[DisableEncodeUrlFilter]
    B --> C[SecurityContextHolderFilter]
    C --> D[HeaderWriterFilter]
    D --> E[CsrfFilter]
    E --> F[LogoutFilter]
    F --> G[認証フィルター群<br/>UsernamePasswordAuthenticationFilter<br/>BasicAuthenticationFilter等]
    G --> H[RequestCacheAwareFilter]
    H --> I[AnonymousAuthenticationFilter]
    I --> J[ExceptionTranslationFilter]
    J --> K[AuthorizationFilter]
    K --> L[DispatcherServlet]
    
    J -->|AuthenticationException| M[AuthenticationEntryPoint]
    J -->|AccessDeniedException| N[AccessDeniedHandler]

カスタムエラーハンドラの実装

Spring Securityでは、認証エラーと認可エラーをそれぞれ異なるハンドラで処理します。カスタムハンドラを実装することで、詳細なエラー情報のログ出力やカスタムレスポンスの返却が可能になります。

AuthenticationEntryPointの実装

AuthenticationEntryPointは、認証されていないユーザーが保護されたリソースにアクセスした際に呼び出されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URI;
import java.time.Instant;

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private static final Logger log = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);
    private final ObjectMapper objectMapper;

    public CustomAuthenticationEntryPoint(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        
        // 詳細なデバッグ情報をログ出力
        log.warn("認証エラー発生 - URI: {}, Method: {}, RemoteAddr: {}, Exception: {}",
                request.getRequestURI(),
                request.getMethod(),
                request.getRemoteAddr(),
                authException.getMessage());
        
        log.debug("認証エラー詳細", authException);

        // Problem Details形式でレスポンスを返却
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
                HttpStatus.UNAUTHORIZED,
                "認証が必要です"
        );
        problemDetail.setTitle("Unauthorized");
        problemDetail.setType(URI.create("https://example.com/errors/unauthorized"));
        problemDetail.setProperty("timestamp", Instant.now().toString());
        problemDetail.setProperty("path", request.getRequestURI());

        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(problemDetail));
    }
}

AccessDeniedHandlerの実装

AccessDeniedHandlerは、認証済みユーザーがアクセス権限のないリソースにアクセスした際に呼び出されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URI;
import java.time.Instant;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private static final Logger log = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);
    private final ObjectMapper objectMapper;

    public CustomAccessDeniedHandler(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication != null ? authentication.getName() : "anonymous";
        String authorities = authentication != null ? authentication.getAuthorities().toString() : "[]";

        // 詳細なデバッグ情報をログ出力
        log.warn("アクセス拒否 - User: {}, Authorities: {}, URI: {}, Method: {}, Exception: {}",
                username,
                authorities,
                request.getRequestURI(),
                request.getMethod(),
                accessDeniedException.getMessage());
        
        log.debug("アクセス拒否詳細", accessDeniedException);

        // Problem Details形式でレスポンスを返却
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
                HttpStatus.FORBIDDEN,
                "このリソースへのアクセス権限がありません"
        );
        problemDetail.setTitle("Forbidden");
        problemDetail.setType(URI.create("https://example.com/errors/forbidden"));
        problemDetail.setProperty("timestamp", Instant.now().toString());
        problemDetail.setProperty("path", request.getRequestURI());

        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(problemDetail));
    }
}

SecurityConfigへの組み込み

カスタムハンドラをSecurityFilterChainに登録します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomAuthenticationEntryPoint authenticationEntryPoint;
    private final CustomAccessDeniedHandler accessDeniedHandler;

    public SecurityConfig(CustomAuthenticationEntryPoint authenticationEntryPoint,
                          CustomAccessDeniedHandler accessDeniedHandler) {
        this.authenticationEntryPoint = authenticationEntryPoint;
        this.accessDeniedHandler = accessDeniedHandler;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler)
            );

        return http.build();
    }
}

例外処理フローの理解

Spring Securityの例外処理はExceptionTranslationFilterが担当します。このフィルターの動作を理解することで、エラーの原因特定が容易になります。

ExceptionTranslationFilterの処理フロー

flowchart TD
    A[ExceptionTranslationFilter] --> B{例外発生?}
    B -->|No| C[正常終了]
    B -->|Yes| D{例外の種類}
    D -->|AuthenticationException| E[SecurityContext<br/>クリア]
    E --> F[リクエストを<br/>RequestCacheに保存]
    F --> G[AuthenticationEntryPoint<br/>呼び出し]
    D -->|AccessDeniedException| H{認証済み?}
    H -->|No| E
    H -->|Yes| I[AccessDeniedHandler<br/>呼び出し]

主要な例外クラス

Spring Securityで発生する主要な例外を理解しておくと、ログの解析が容易になります。

例外クラス 発生条件 対応するハンドラ
BadCredentialsException 認証情報(ユーザー名/パスワード)が不正 AuthenticationEntryPoint
UsernameNotFoundException ユーザーが見つからない AuthenticationEntryPoint
AccountExpiredException アカウントの有効期限切れ AuthenticationEntryPoint
LockedException アカウントがロック状態 AuthenticationEntryPoint
DisabledException アカウントが無効化されている AuthenticationEntryPoint
CredentialsExpiredException 認証情報の有効期限切れ AuthenticationEntryPoint
AccessDeniedException アクセス権限がない AccessDeniedHandler
InsufficientAuthenticationException 認証レベルが不足 AuthenticationEntryPoint

よくある設定ミスと対処法

Spring Securityの設定でよく遭遇する問題とその解決方法を解説します。

401 Unauthorizedが返される

認証情報を送信しているにもかかわらず401エラーが返される場合の確認ポイントです。

症状

1
2
2026-01-13T10:30:00.000+09:00 DEBUG o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
2026-01-13T10:30:00.001+09:00 DEBUG o.s.s.w.a.ExceptionTranslationFilter : Sending 401 due to AuthenticationException

原因1: 認証フィルターが登録されていない

1
2
3
4
5
6
7
8
// 誤った設定 - httpBasicが有効化されていない
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    // httpBasic()やformLogin()の設定がない
    return http.build();
}
1
2
3
4
5
6
7
8
// 正しい設定
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .httpBasic(Customizer.withDefaults()); // Basic認証を有効化
    return http.build();
}

原因2: Authorizationヘッダーの形式が不正

1
2
3
4
5
# 誤った形式
curl -H "Authorization: Basic123456" http://localhost:8080/api/users

# 正しい形式(Base64エンコード)
curl -H "Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=" http://localhost:8080/api/users

403 Forbiddenが返される

認証は成功するが、特定のエンドポイントで403エラーが返される場合の確認ポイントです。

症状

1
2
3
2026-01-13T10:35:00.000+09:00 DEBUG o.s.s.w.a.i.AuthorizationFilter : Authorizing GET /api/admin/users
2026-01-13T10:35:00.001+09:00 DEBUG o.s.s.w.a.i.AuthorizationFilter : Failed to authorize GET /api/admin/users
2026-01-13T10:35:00.002+09:00 DEBUG o.s.s.w.a.AccessDeniedHandlerImpl : Responding with 403 status code

原因1: ロール名のプレフィックス問題

1
2
3
4
5
6
7
8
9
// 誤った設定 - ROLEプレフィックスの二重付与
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/admin/**").hasRole("ROLE_ADMIN") // 誤り
        );
    return http.build();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 正しい設定
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/admin/**").hasRole("ADMIN") // hasRoleは自動でROLE_を付与
            // または
            .requestMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN") // hasAuthorityは完全一致
        );
    return http.build();
}

原因2: リクエストマッチャーの順序問題

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 誤った設定 - より広いパターンが先に評価される
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .anyRequest().authenticated() // すべてのリクエストがここでマッチ
            .requestMatchers("/api/public/**").permitAll() // 到達しない
        );
    return http.build();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 正しい設定 - 具体的なパターンを先に配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/public/**").permitAll()
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        );
    return http.build();
}

CSRFトークンエラー

POSTリクエストで403エラーが返される場合、CSRFトークンの問題が考えられます。

症状

1
2026-01-13T10:40:00.000+09:00 DEBUG o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/api/users

原因と対処法

1
2
3
4
5
6
7
8
// REST APIでCSRFを無効化する場合
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable()) // ステートレスAPIでは無効化が一般的
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// SPAでCSRFを使用する場合(Cookieベース)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
        )
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}

CORSエラー

ブラウザからのリクエストでCORSエラーが発生する場合の対処法です。

症状

ブラウザのコンソールに以下のエラーが表示されます。

1
2
Access to XMLHttpRequest at 'http://localhost:8080/api/users' from origin 'http://localhost:3000' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

対処法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .cors(cors -> cors.configurationSource(corsConfigurationSource()))
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("http://localhost:3000"));
    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowedHeaders(List.of("*"));
    configuration.setAllowCredentials(true);
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

認証フィルターの重複実行

カスタムフィルターが2回実行される問題の対処法です。

症状

1
2
2026-01-13T10:45:00.000+09:00 DEBUG CustomJwtFilter : JWT検証実行
2026-01-13T10:45:00.100+09:00 DEBUG CustomJwtFilter : JWT検証実行  // 2回目

原因と対処法

カスタムフィルターを@Componentとして登録すると、Spring Bootが自動でServletコンテナにも登録します。

1
2
3
4
5
6
7
8
// 誤った設定
@Component  // Spring Beanとして登録される
public class CustomJwtFilter extends OncePerRequestFilter {
    // ...
}

// SecurityConfigでも追加
http.addFilterBefore(new CustomJwtFilter(), UsernamePasswordAuthenticationFilter.class);
1
2
3
4
5
6
7
// 正しい設定 - FilterRegistrationBeanで自動登録を無効化
@Bean
public FilterRegistrationBean<CustomJwtFilter> jwtFilterRegistration(CustomJwtFilter filter) {
    FilterRegistrationBean<CustomJwtFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false);  // Servletコンテナへの自動登録を無効化
    return registration;
}

デバッグ用ユーティリティの実装

開発環境でのデバッグを効率化するためのユーティリティクラスを紹介します。

リクエスト情報ロギングフィルター

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Collections;

public class SecurityDebugFilter extends OncePerRequestFilter {

    private static final Logger log = LoggerFactory.getLogger(SecurityDebugFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        
        // リクエスト前の情報をログ出力
        log.debug("=== Security Debug Start ===");
        log.debug("Request: {} {}", request.getMethod(), request.getRequestURI());
        log.debug("Query: {}", request.getQueryString());
        
        // ヘッダー情報
        Collections.list(request.getHeaderNames()).forEach(headerName ->
            log.debug("Header: {} = {}", headerName, request.getHeader(headerName))
        );

        try {
            filterChain.doFilter(request, response);
        } finally {
            // レスポンス後の情報をログ出力
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            log.debug("Authentication: {}", auth != null ? auth.getName() : "null");
            log.debug("Authorities: {}", auth != null ? auth.getAuthorities() : "[]");
            log.debug("Response Status: {}", response.getStatus());
            log.debug("=== Security Debug End ===");
        }
    }
}

SecurityConfigへの組み込み(開発環境のみ)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Value("${app.security.debug:false}")
    private boolean securityDebugEnabled;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        if (securityDebugEnabled) {
            http.addFilterBefore(new SecurityDebugFilter(), DisableEncodeUrlFilter.class);
        }
        
        http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .httpBasic(Customizer.withDefaults());
        
        return http.build();
    }
}
1
2
# application-dev.properties
app.security.debug=true

トラブルシューティングチェックリスト

問題発生時に確認すべき項目をチェックリストとしてまとめます。

認証エラー(401)の場合

  1. ログレベルをDEBUGに設定し、認証処理のログを確認
  2. 認証フィルター(httpBasic、formLogin等)が有効化されているか確認
  3. UserDetailsServiceが正しく設定されているか確認
  4. PasswordEncoderが設定されているか確認
  5. Authorizationヘッダーの形式が正しいか確認

認可エラー(403)の場合

  1. ログでどの認可ルールにマッチしたか確認
  2. ユーザーに付与されている権限をログで確認
  3. hasRole/hasAuthorityの使い分けを確認
  4. リクエストマッチャーの順序を確認
  5. CSRFトークンが必要な場合は正しく送信されているか確認

その他のエラーの場合

  1. SecurityFilterChainに登録されているフィルター一覧を確認
  2. カスタムフィルターの実行順序を確認
  3. 例外の種類とスタックトレースを確認
  4. CORSの設定を確認(ブラウザからのアクセスの場合)

まとめ

本記事では、Spring Securityのデバッグ手法について以下の内容を解説しました。

  • デバッグログの有効化: logging.level.org.springframework.security=DEBUGを設定することで、認証・認可の処理フローを追跡できます
  • カスタムエラーハンドラ: AuthenticationEntryPointとAccessDeniedHandlerをカスタマイズすることで、詳細なエラー情報のログ出力とカスタムレスポンスの返却が可能になります
  • よくある設定ミス: ロール名のプレフィックス問題、リクエストマッチャーの順序、CSRF/CORSの設定など、頻出する問題と対処法を理解することで迅速な問題解決が可能になります

Spring Securityの問題解決において最も重要なのは、ログを適切に活用することです。DEBUGレベルのログを有効化し、フィルターの実行順序や認証・認可の判定過程を追跡することで、多くの問題を効率的に特定できます。

参考リンク