ログアウト処理は認証機能の重要な一部であり、適切に実装しないとセッションハイジャックやセキュリティトークンの漏洩といったリスクにつながります。本記事では、Spring Securityのlogout()設定オプション、SecurityContextHolderの仕組み、セッション・Cookie・トークンの無効化処理、そしてカスタムLogoutHandlerの実装方法を解説します。

実行環境と前提条件

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

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

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

  • Spring Securityの基本概念(認証・認可の違い)
  • SecurityFilterChainの設定方法
  • HTTPセッションとCookieの基本

SecurityContextHolderの仕組み

ログアウト処理を理解するには、まずSpring Securityが認証情報をどのように管理しているかを把握する必要があります。SecurityContextHolderは認証済みユーザーの情報を保持する中核コンポーネントです。

SecurityContextHolderのアーキテクチャ

SecurityContextHolderは、SecurityContextを保持し、SecurityContextAuthenticationオブジェクトを含みます。この階層構造によって認証情報が管理されます。

flowchart TB
    A[SecurityContextHolder] --> B[SecurityContext]
    B --> C[Authentication]
    C --> D[Principal<br/>ユーザー情報]
    C --> E[Credentials<br/>資格情報]
    C --> F[Authorities<br/>権限]

各コンポーネントの役割は以下のとおりです。

コンポーネント 役割
SecurityContextHolder SecurityContextの保存・取得を管理するコンテナ
SecurityContext 現在認証されているユーザーのAuthenticationを保持
Authentication ユーザーの識別情報、資格情報、権限を表現

SecurityContextHolderの保存戦略

SecurityContextHolderはデフォルトでThreadLocalを使用して認証情報を保存します。これにより、同じスレッド内のどこからでも認証情報にアクセスできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

// 現在の認証情報を取得
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();

if (authentication != null && authentication.isAuthenticated()) {
    String username = authentication.getName();
    Object principal = authentication.getPrincipal();
    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
}

SecurityContextHolderの保存戦略は以下の3種類があります。

戦略 説明 ユースケース
MODE_THREADLOCAL スレッドごとに独立したコンテキスト(デフォルト) 一般的なWebアプリケーション
MODE_INHERITABLETHREADLOCAL 子スレッドに親のコンテキストを継承 非同期処理で認証情報を引き継ぐ場合
MODE_GLOBAL JVM全体で1つのコンテキストを共有 スタンドアロンアプリケーション

保存戦略の変更は、システムプロパティまたは静的メソッドで設定できます。

1
2
3
4
5
// システムプロパティで設定
System.setProperty("spring.security.strategy", "MODE_INHERITABLETHREADLOCAL");

// または静的メソッドで設定
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

ログアウト時のSecurityContext処理

ログアウト時には、SecurityContextHolderから認証情報をクリアする必要があります。これを行わないと、同じスレッドを再利用した際に前のユーザーの認証情報が残ってしまう危険性があります。

sequenceDiagram
    participant User as ユーザー
    participant Filter as LogoutFilter
    participant Handler as SecurityContextLogoutHandler
    participant Holder as SecurityContextHolder
    participant Repo as SecurityContextRepository

    User->>Filter: POST /logout
    Filter->>Handler: logout()
    Handler->>Holder: clearContext()
    Handler->>Repo: saveContext(emptyContext)
    Handler-->>Filter: 完了
    Filter-->>User: リダイレクト(/login?logout)

Spring Securityのログアウトアーキテクチャ

Spring Securityのログアウト機能は、LogoutFilterによって処理されます。LogoutFilterは複数のLogoutHandlerを順番に実行し、最後にLogoutSuccessHandlerを呼び出します。

ログアウト処理の流れ

flowchart TB
    A[POST /logout] --> B[LogoutFilter]
    B --> C{URLがマッチ?}
    C -->|No| D[次のFilterへ]
    C -->|Yes| E[LogoutHandlers実行]
    E --> F[SecurityContextLogoutHandler]
    F --> G[CsrfLogoutHandler]
    G --> H[CookieClearingLogoutHandler]
    H --> I[LogoutSuccessHandler]
    I --> J[リダイレクトまたはレスポンス]

デフォルトのLogoutHandler

Spring Security 6.xでは、以下のLogoutHandlerがデフォルトで登録されています。

LogoutHandler 役割
SecurityContextLogoutHandler セッションの無効化、SecurityContextHolderのクリア、SecurityContextRepositoryのクリア
CsrfLogoutHandler 保存されたCSRFトークンを削除
LogoutSuccessEventPublishingLogoutHandler LogoutSuccessEventを発行

Remember-Me認証を使用している場合は、追加でTokenRememberMeServicesまたはPersistentTokenRememberMeServicesが登録されます。

logout()の設定オプション

SecurityFilterChainlogout()を使用して、ログアウト処理をカスタマイズできます。

基本的なログアウト設定

最もシンプルなログアウト設定は以下のとおりです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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 {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults())
            .logout(Customizer.withDefaults());
        
        return http.build();
    }
}

Customizer.withDefaults()を使用した場合、以下のデフォルト動作が適用されます。

項目 デフォルト値
ログアウトURL /logout(GETで確認ページ、POSTで実行)
ログアウト成功後のリダイレクト先 /login?logout
セッション無効化 有効
SecurityContextクリア 有効
CSRF保護 有効(POSTリクエストが必要)

詳細なログアウト設定

ログアウトURLやリダイレクト先をカスタマイズする場合は、以下のように設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .requestMatchers("/public/**").permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(form -> form
            .loginPage("/login")
            .permitAll()
        )
        .logout(logout -> logout
            .logoutUrl("/api/logout")                          // ログアウトを処理するURL
            .logoutSuccessUrl("/login?logout")                 // ログアウト成功後のリダイレクト先
            .invalidateHttpSession(true)                       // セッションを無効化
            .clearAuthentication(true)                         // Authentication をクリア
            .deleteCookies("JSESSIONID", "remember-me")        // 指定したCookieを削除
            .permitAll()                                       // ログアウトURLへのアクセスを許可
        );
    
    return http.build();
}

各設定オプションの詳細は以下のとおりです。

メソッド 説明 デフォルト値
logoutUrl(String) ログアウトを処理するURL /logout
logoutSuccessUrl(String) ログアウト成功後のリダイレクト先 /login?logout
invalidateHttpSession(boolean) セッションを無効化するか true
clearAuthentication(boolean) Authenticationをクリアするか true
deleteCookies(String...) 削除するCookie名を指定 なし
permitAll() ログアウト関連URLへのアクセスを許可 -

REST APIでのログアウト設定

REST APIでは、リダイレクトではなくHTTPステータスコードを返すことが一般的です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.http.HttpStatus;

@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .csrf(csrf -> csrf.disable())
        .logout(logout -> logout
            .logoutUrl("/api/logout")
            .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
        );
    
    return http.build();
}

この設定では、ログアウト成功時に200 OKのステータスコードのみを返します。

セッション・Cookie・トークンの無効化

安全なログアウト処理には、セッション、Cookie、認証トークンを適切に無効化する必要があります。

セッションの無効化

SecurityContextLogoutHandlerは、デフォルトでHTTPセッションを無効化します。この動作はinvalidateHttpSession(true)で制御されます。

1
2
3
4
5
6
7
8
9
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .logout(logout -> logout
            .invalidateHttpSession(true)  // セッションを無効化(デフォルト: true)
        );
    
    return http.build();
}

セッション無効化時に実行される処理は以下のとおりです。

  1. HttpSession.invalidate()の呼び出し
  2. セッションに紐づくすべての属性の削除
  3. セッションIDの無効化

Cookieの削除

deleteCookies()メソッドで、ログアウト時に削除するCookieを指定できます。

1
2
3
4
5
6
7
8
9
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .logout(logout -> logout
            .deleteCookies("JSESSIONID", "remember-me", "custom-cookie")
        );
    
    return http.build();
}

内部的にはCookieClearingLogoutHandlerが使用されます。手動で追加することも可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    CookieClearingLogoutHandler cookies = 
        new CookieClearingLogoutHandler("custom-cookie-1", "custom-cookie-2");
    
    http
        .logout(logout -> logout
            .addLogoutHandler(cookies)
        );
    
    return http.build();
}

Clear-Site-Dataヘッダーによるクリーンアップ

モダンブラウザでは、Clear-Site-Dataヘッダーを使用してCookie、ストレージ、キャッシュを一括でクリアできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // すべてのサイトデータをクリア
    HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(
        new ClearSiteDataHeaderWriter(Directive.ALL)
    );
    
    http
        .logout(logout -> logout
            .addLogoutHandler(clearSiteData)
        );
    
    return http.build();
}

Directiveで指定できる値は以下のとおりです。

Directive クリア対象
CACHE ブラウザキャッシュ
COOKIES Cookie
STORAGE localStorage、sessionStorage、IndexedDBなど
EXECUTION_CONTEXTS Service Workerなど
ALL 上記すべて

Cookieのみをクリアする設定例です。

1
2
3
4
5
6
7
8
HeaderWriterLogoutHandler clearCookies = new HeaderWriterLogoutHandler(
    new ClearSiteDataHeaderWriter(Directive.COOKIES)
);

http
    .logout(logout -> logout
        .addLogoutHandler(clearCookies)
    );

Remember-Meトークンの無効化

Remember-Me認証を使用している場合、ログアウト時にトークンも無効化する必要があります。Spring Securityでは、Remember-Meを設定すると自動的にログアウト時のトークン削除が行われます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .rememberMe(remember -> remember
            .key("uniqueAndSecret")
            .tokenValiditySeconds(86400)
        )
        .logout(logout -> logout
            .deleteCookies("remember-me")  // Remember-Me Cookieを明示的に削除
        );
    
    return http.build();
}

カスタムLogoutHandlerの実装

アプリケーション固有のログアウト処理が必要な場合、LogoutHandlerインターフェースを実装してカスタムハンドラーを作成できます。

LogoutHandlerインターフェース

LogoutHandlerは関数型インターフェースであり、ラムダ式でも実装できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;

@FunctionalInterface
public interface LogoutHandler {
    void logout(HttpServletRequest request, 
                HttpServletResponse response, 
                Authentication authentication);
}

ログ記録用カスタムLogoutHandler

ログアウト時に監査ログを記録するカスタムハンドラーの実装例です。

 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 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.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;

@Component
public class AuditLogoutHandler implements LogoutHandler {

    private static final Logger logger = LoggerFactory.getLogger(AuditLogoutHandler.class);

    @Override
    public void logout(HttpServletRequest request, 
                       HttpServletResponse response, 
                       Authentication authentication) {
        if (authentication != null) {
            String username = authentication.getName();
            String ipAddress = getClientIpAddress(request);
            String sessionId = request.getRequestedSessionId();
            
            logger.info("User logout - username: {}, IP: {}, sessionId: {}", 
                        username, ipAddress, sessionId);
        }
    }

    private String getClientIpAddress(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }
}

トークン無効化用カスタムLogoutHandler

JWTのRefresh Tokenやカスタムトークンを無効化するハンドラーの例です。

 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
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;

@Component
public class TokenInvalidationLogoutHandler implements LogoutHandler {

    private final TokenRepository tokenRepository;

    public TokenInvalidationLogoutHandler(TokenRepository tokenRepository) {
        this.tokenRepository = tokenRepository;
    }

    @Override
    public void logout(HttpServletRequest request, 
                       HttpServletResponse response, 
                       Authentication authentication) {
        // Authorizationヘッダーからトークンを取得
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7);
            // トークンをブラックリストに追加または無効化
            tokenRepository.invalidateToken(token);
        }

        // ユーザーの全Refresh Tokenを無効化
        if (authentication != null) {
            String username = authentication.getName();
            tokenRepository.invalidateAllUserTokens(username);
        }
    }
}

カスタムLogoutHandlerの登録

カスタムLogoutHandleraddLogoutHandler()メソッドで登録します。

 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
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final AuditLogoutHandler auditLogoutHandler;
    private final TokenInvalidationLogoutHandler tokenInvalidationLogoutHandler;

    public SecurityConfig(AuditLogoutHandler auditLogoutHandler,
                          TokenInvalidationLogoutHandler tokenInvalidationLogoutHandler) {
        this.auditLogoutHandler = auditLogoutHandler;
        this.tokenInvalidationLogoutHandler = tokenInvalidationLogoutHandler;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .logout(logout -> logout
                .logoutUrl("/logout")
                .addLogoutHandler(auditLogoutHandler)
                .addLogoutHandler(tokenInvalidationLogoutHandler)
                .logoutSuccessUrl("/login?logout")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
            );
        
        return http.build();
    }
}

LogoutHandlerの実行順序は、追加した順番に依存します。LogoutHandlerは例外をスローすべきではありません。

ラムダ式によるカスタムLogoutHandler

シンプルな処理であれば、ラムダ式でインラインに記述できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .logout(logout -> logout
            .addLogoutHandler((request, response, authentication) -> {
                if (authentication != null) {
                    System.out.println("Logging out user: " + authentication.getName());
                }
            })
        );
    
    return http.build();
}

カスタムLogoutSuccessHandlerの実装

ログアウト成功後の処理をカスタマイズするには、LogoutSuccessHandlerを実装します。

LogoutSuccessHandlerインターフェース

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import java.io.IOException;

public interface LogoutSuccessHandler {
    void onLogoutSuccess(HttpServletRequest request, 
                         HttpServletResponse response, 
                         Authentication authentication) 
            throws IOException, ServletException;
}

JSONレスポンスを返すLogoutSuccessHandler

REST APIでJSONレスポンスを返す実装例です。

 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 com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;

@Component
public class JsonLogoutSuccessHandler implements LogoutSuccessHandler {

    private final ObjectMapper objectMapper;

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

    @Override
    public void onLogoutSuccess(HttpServletRequest request, 
                                HttpServletResponse response, 
                                Authentication authentication) throws IOException {
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        
        Map<String, Object> responseBody = Map.of(
            "status", "success",
            "message", "ログアウトが完了しました",
            "timestamp", System.currentTimeMillis()
        );
        
        objectMapper.writeValue(response.getOutputStream(), responseBody);
    }
}

LogoutSuccessHandlerの登録

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .logout(logout -> logout
            .logoutUrl("/api/logout")
            .logoutSuccessHandler(jsonLogoutSuccessHandler)
        );
    
    return http.build();
}

カスタムログアウトエンドポイントの作成

LogoutFilterを使わずに、Spring MVCのコントローラーでログアウト処理を行うことも可能です。ただし、この場合はSecurityContextLogoutHandlerを明示的に呼び出す必要があります。

コントローラーでのログアウト実装

 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
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LogoutController {

    private final SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();

    @PostMapping("/custom/logout")
    public Map<String, String> performLogout(HttpServletRequest request, 
                                              HttpServletResponse response) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        if (authentication != null) {
            // SecurityContextLogoutHandlerでログアウト処理を実行
            logoutHandler.logout(request, response, authentication);
        }
        
        return Map.of("message", "ログアウトしました");
    }
}

SecurityContextLogoutHandlerを呼び出さないと、SecurityContextが残ったままになり、実質的にログアウトされない状態になるため、注意が必要です。

SecurityFilterChainでカスタムエンドポイントを許可

カスタムログアウトエンドポイントを使用する場合、AuthorizationFilterで許可する必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .requestMatchers("/custom/logout").permitAll()
            .anyRequest().authenticated()
        )
        .logout(logout -> logout
            .disable()  // デフォルトのLogoutFilterを無効化
        );
    
    return http.build();
}

ログアウト処理のテスト

Spring Security Test を使用して、ログアウト処理をテストできます。

MockMvcによるログアウトテスト

 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
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
class LogoutIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockUser(username = "testuser")
    void logout_shouldInvalidateSessionAndRedirect() throws Exception {
        mockMvc.perform(post("/logout")
                .with(csrf()))
            .andExpect(status().is3xxRedirection())
            .andExpect(redirectedUrl("/login?logout"));
    }

    @Test
    @WithMockUser(username = "testuser")
    void logout_shouldClearSecurityContext() throws Exception {
        // ログアウト実行
        mockMvc.perform(post("/logout")
                .with(csrf()))
            .andExpect(status().is3xxRedirection());

        // ログアウト後、保護されたリソースにアクセスできないことを確認
        mockMvc.perform(post("/protected-resource"))
            .andExpect(status().isUnauthorized());
    }
}

REST APIのログアウトテスト

1
2
3
4
5
6
7
8
@Test
@WithMockUser(username = "apiuser")
void apiLogout_shouldReturnOkStatus() throws Exception {
    mockMvc.perform(post("/api/logout")
            .with(csrf()))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.status").value("success"));
}

まとめ

Spring Securityのログアウト処理とSecurityContextの管理について解説しました。要点をまとめます。

項目 内容
SecurityContextHolder 認証情報をThreadLocalで管理し、ログアウト時にクリアが必要
デフォルトのログアウト処理 セッション無効化、SecurityContextクリア、CSRFトークン削除を自動実行
logout()設定 logoutUrllogoutSuccessUrldeleteCookiesなどでカスタマイズ可能
LogoutHandler クリーンアップ処理を追加するためのインターフェース
LogoutSuccessHandler ログアウト成功後の処理をカスタマイズ
Clear-Site-Data モダンブラウザでCookie、ストレージ、キャッシュを一括クリア

安全なログアウト処理を実装するには、セッション、Cookie、認証トークンのすべてを適切に無効化し、SecurityContextを確実にクリアすることが重要です。

参考リンク