セッションベースの認証は、Webアプリケーションにおいて最も一般的な認証方式です。しかし、セッション管理を適切に設定しないと、セッション固定攻撃(Session Fixation)やセッションハイジャックなどのセキュリティリスクにさらされます。本記事では、Spring SecurityのsessionManagement()を使用して、安全なセッション管理を実装する方法を解説します。

実行環境と前提条件

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

項目 バージョン・要件
Java 17以上
Spring Boot 3.4.x
Spring Security 6.4.x
Spring Session 3.4.x(分散環境の場合)
Redis 7.x(Spring Session使用時)
ビルドツール Maven または Gradle

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

  • Spring Securityの基本的な設定方法
  • SecurityFilterChainの仕組み
  • HTTPセッションの基本概念

セッション管理の重要性

セッションIDは、認証済みユーザーを識別するための「合言葉」です。このセッションIDが漏洩または推測されると、攻撃者はそのユーザーになりすますことができます。

flowchart LR
    subgraph 正常な流れ
        A[ユーザー] -->|セッションID| B[サーバー]
        B -->|認証済みレスポンス| A
    end
    subgraph 攻撃シナリオ
        C[攻撃者] -->|盗んだセッションID| D[サーバー]
        D -->|被害者としてレスポンス| C
    end

Spring Securityは、セッション管理に関する以下の機能を提供しています。

機能 説明
セッション固定攻撃対策 認証時にセッションIDを変更し、攻撃を防止
同時セッション制御 ユーザーあたりの同時ログイン数を制限
セッションタイムアウト検知 セッション期限切れ時の動作をカスタマイズ
セッション生成ポリシー セッションの作成タイミングを制御

sessionManagement()の基本設定

Spring Security 6.xでは、sessionManagement()メソッドを使用してセッション管理を設定します。

基本的な設定例

 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
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 {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .sessionManagement(session -> session
                .sessionFixation(fixation -> fixation
                    .changeSessionId()
                )
                .maximumSessions(1)
                .maxSessionsPreventsLogin(false)
            );

        return http.build();
    }
}

SessionCreationPolicyの設定

SessionCreationPolicyは、Spring Securityがセッションをどのように作成するかを制御します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import org.springframework.security.config.http.SessionCreationPolicy;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
        );
    return http.build();
}

各ポリシーの動作は以下のとおりです。

ポリシー 動作 ユースケース
ALWAYS 常にセッションを作成 セッションが必須の場合
IF_REQUIRED 必要な場合のみ作成(デフォルト) 一般的なWebアプリケーション
NEVER Spring Securityはセッションを作成しないが、存在すれば使用 外部でセッション管理する場合
STATELESS セッションを一切使用しない REST API(JWT認証など)

REST APIでJWT認証を使用する場合の設定例を示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> {})
        );
    return http.build();
}

セッション固定攻撃(Session Fixation)対策

セッション固定攻撃は、攻撃者が事前に取得したセッションIDを被害者に使用させ、そのセッションを乗っ取る攻撃手法です。

攻撃の流れ

sequenceDiagram
    participant Attacker as 攻撃者
    participant Server as サーバー
    participant Victim as 被害者

    Attacker->>Server: 1. サイトにアクセス
    Server-->>Attacker: セッションID: ABC123
    Attacker->>Victim: 2. 悪意あるリンク送付<br>(セッションID: ABC123を含む)
    Victim->>Server: 3. リンクをクリック<br>(セッションID: ABC123でアクセス)
    Victim->>Server: 4. ログイン実行
    Server-->>Victim: ログイン成功<br>(セッションID: ABC123のまま)
    Attacker->>Server: 5. セッションID: ABC123でアクセス
    Server-->>Attacker: 被害者としてレスポンス

Spring Securityによる対策

Spring Securityは、認証成功時にセッションIDを変更することで、この攻撃を防ぎます。デフォルトでchangeSessionIdが有効になっています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .sessionFixation(fixation -> fixation
                .changeSessionId()  // デフォルト(Servlet 3.1以上)
            )
        );
    return http.build();
}

設定可能な戦略は以下のとおりです。

戦略 動作 推奨度
changeSessionId() セッションIDのみ変更、属性は保持 推奨(デフォルト)
migrateSession() 新規セッション作成、属性をコピー Servlet 3.0以前向け
newSession() 新規セッション作成、属性はコピーしない 厳格なセキュリティ要件向け
none() 対策を無効化 非推奨

newSession()を使用する場合の設定例を示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .sessionFixation(fixation -> fixation
                .newSession()  // セッション属性をコピーしない
            )
        );
    return http.build();
}

セッション固定攻撃対策後の動作

対策が有効な場合、認証時にセッションIDが変更されます。

sequenceDiagram
    participant Attacker as 攻撃者
    participant Server as サーバー
    participant Victim as 被害者

    Attacker->>Server: 1. サイトにアクセス
    Server-->>Attacker: セッションID: ABC123
    Attacker->>Victim: 2. 悪意あるリンク送付
    Victim->>Server: 3. リンクをクリック
    Victim->>Server: 4. ログイン実行
    Server-->>Victim: ログイン成功<br>新セッションID: XYZ789に変更
    Attacker->>Server: 5. セッションID: ABC123でアクセス
    Server-->>Attacker: セッション無効エラー

同時セッション数の制限

同一ユーザーが複数のブラウザやデバイスから同時にログインする数を制限できます。この機能は、不正なアカウント共有の防止やセキュリティ強化に有効です。

HttpSessionEventPublisherの登録

同時セッション制御を有効にするには、HttpSessionEventPublisherをBeanとして登録する必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.session.HttpSessionEventPublisher;

@Configuration
public class SessionConfig {

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
}

同時セッション数の設定

1
2
3
4
5
6
7
8
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)  // 同時ログイン数を1に制限
        );
    return http.build();
}

2回目のログイン時の動作制御

2回目のログインが発生した場合の動作を制御できます。

1
2
3
4
5
6
7
8
9
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)
            .maxSessionsPreventsLogin(false)  // 古いセッションを無効化(デフォルト)
        );
    return http.build();
}
設定 動作
maxSessionsPreventsLogin(false) 新しいログインを許可し、古いセッションを無効化
maxSessionsPreventsLogin(true) 新しいログインを拒否

ロールベースでの同時セッション数制御

Spring Security 6.3以降では、ユーザーの権限に基づいて同時セッション数を動的に変更できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.authorization.AuthorizationManager;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    AuthorizationManager<?> isAdmin = AuthorityAuthorizationManager.hasRole("ADMIN");
    
    http
        .sessionManagement(session -> session
            .maximumSessions(authentication -> 
                isAdmin.authorize(() -> authentication, null).isGranted() ? -1 : 1
            )
        );
    return http.build();
}

この設定では、管理者(ADMIN)は無制限(-1)、一般ユーザーは1セッションに制限されます。

セッション無効化時の通知

古いセッションが無効化された場合の処理をカスタマイズできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)
            .expiredSessionStrategy(event -> {
                HttpServletResponse response = event.getResponse();
                response.setContentType("application/json;charset=UTF-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write(
                    "{\"error\": \"session_expired\", \"message\": \"別のデバイスからログインされました\"}"
                );
            })
        );
    return http.build();
}

UserDetailsのequals/hashCodeの実装

同時セッション制御が正しく動作するためには、UserDetails実装クラスでequals()hashCode()を適切にオーバーライドする必要があります。

 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
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Objects;

public class CustomUserDetails implements UserDetails {
    
    private final Long id;
    private final String username;
    private final String password;
    private final Collection<? extends GrantedAuthority> authorities;

    // コンストラクタ、getter省略

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CustomUserDetails that = (CustomUserDetails) o;
        return Objects.equals(username, that.username);
    }

    @Override
    public int hashCode() {
        return Objects.hash(username);
    }
    
    // その他のUserDetailsメソッド省略
}

セッションタイムアウトの設定

セッションタイムアウトは、一定時間操作がないセッションを自動的に無効化する機能です。

application.propertiesでの設定

1
2
3
4
5
6
7
# セッションタイムアウト(30分)
server.servlet.session.timeout=30m

# Cookieの設定
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.same-site=lax

application.ymlでの設定

1
2
3
4
5
6
7
8
server:
  servlet:
    session:
      timeout: 30m
      cookie:
        http-only: true
        secure: true
        same-site: lax

タイムアウト検知と無効セッション処理

セッションがタイムアウトした場合のリダイレクト先を設定できます。

1
2
3
4
5
6
7
8
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .invalidSessionUrl("/session-expired")
        );
    return http.build();
}

カスタム戦略を使用する場合は以下のように設定します。

 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
import org.springframework.security.web.session.InvalidSessionStrategy;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .invalidSessionStrategy(new CustomInvalidSessionStrategy())
        );
    return http.build();
}

public class CustomInvalidSessionStrategy implements InvalidSessionStrategy {
    
    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, 
                                          HttpServletResponse response) 
            throws IOException {
        
        String requestedWith = request.getHeader("X-Requested-With");
        
        if ("XMLHttpRequest".equals(requestedWith)) {
            // Ajax リクエストの場合
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"error\": \"session_timeout\"}");
        } else {
            // 通常リクエストの場合
            response.sendRedirect("/login?expired");
        }
    }
}

ログアウト時のセッションCookieクリア

ログアウト時にセッション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 static org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive.*;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .logout(logout -> logout
            .logoutUrl("/logout")
            .logoutSuccessUrl("/login?logout")
            .addLogoutHandler(new HeaderWriterLogoutHandler(
                new ClearSiteDataHeaderWriter(COOKIES)
            ))
            .invalidateHttpSession(true)
            .clearAuthentication(true)
        );
    return http.build();
}

deleteCookies()を使用する方法もあります。

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

分散環境でのセッション管理(Spring Session)

複数のアプリケーションサーバーで構成される分散環境では、セッション情報を共有ストレージで管理する必要があります。Spring Sessionを使用することで、この課題を解決できます。

Spring Sessionの特徴

flowchart TB
    subgraph クライアント
        A[ブラウザ]
    end
    subgraph ロードバランサー
        B[LB]
    end
    subgraph アプリケーションサーバー
        C[Server 1]
        D[Server 2]
        E[Server N]
    end
    subgraph セッションストア
        F[(Redis)]
    end

    A --> B
    B --> C
    B --> D
    B --> E
    C <--> F
    D <--> F
    E <--> F

Spring Sessionが提供する主な機能は以下のとおりです。

機能 説明
透過的なセッション管理 アプリケーションコードの変更なしでセッション共有を実現
複数のストア対応 Redis、JDBC、Hazelcast、MongoDBなど
セッションイベント セッション作成、破棄のイベントをサポート
RESTfulセッション HTTPヘッダーでのセッションID送受信

Spring Session + Redisの導入

依存関係を追加します。

Mavenの場合(pom.xml):

1
2
3
4
5
6
7
8
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

Gradleの場合(build.gradle):

1
2
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.session:spring-session-data-redis'

Redis接続設定

application.ymlで設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: ${REDIS_PASSWORD:}
      timeout: 2000ms
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0
  session:
    store-type: redis
    timeout: 30m
    redis:
      namespace: myapp:session

Spring Sessionの有効化

1
2
3
4
5
6
7
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)  // 30分
public class SessionConfig {
}

SecurityFilterChainとの統合

Spring SessionはSpring Securityと自動的に統合されます。追加の設定は不要ですが、同時セッション制御を使用する場合はSpringSessionBackedSessionRegistryを設定します。

 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
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;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final FindByIndexNameSessionRepository<? extends Session> sessionRepository;

    public SecurityConfig(FindByIndexNameSessionRepository<? extends Session> sessionRepository) {
        this.sessionRepository = sessionRepository;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .sessionManagement(session -> session
                .maximumSessions(1)
                .sessionRegistry(sessionRegistry())
            );

        return http.build();
    }

    @Bean
    public SpringSessionBackedSessionRegistry<? extends Session> sessionRegistry() {
        return new SpringSessionBackedSessionRegistry<>(sessionRepository);
    }
}

JDBC対応(Spring Session JDBC)

Redisが使用できない環境では、JDBCを使用したセッション管理も可能です。

依存関係を追加します。

1
2
3
4
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-jdbc</artifactId>
</dependency>

application.ymlで設定します。

1
2
3
4
5
6
spring:
  session:
    store-type: jdbc
    jdbc:
      initialize-schema: always
      table-name: SPRING_SESSION

Spring Session JDBCは、以下のテーブルを自動作成します。

テーブル 用途
SPRING_SESSION セッション情報
SPRING_SESSION_ATTRIBUTES セッション属性

セッション管理のベストプラクティス

本番環境でセッション管理を安全に運用するためのベストプラクティスをまとめます。

推奨設定の全体像

 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
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;
import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import static org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive.*;

@Configuration
@EnableWebSecurity
public class ProductionSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/public/**", "/login", "/error").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true)
                .failureUrl("/login?error")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .addLogoutHandler(new HeaderWriterLogoutHandler(
                    new ClearSiteDataHeaderWriter(COOKIES)
                ))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
            )
            .sessionManagement(session -> session
                // セッション固定攻撃対策
                .sessionFixation(fixation -> fixation
                    .changeSessionId()
                )
                // 同時セッション数制限
                .maximumSessions(1)
                .maxSessionsPreventsLogin(false)
                .expiredUrl("/login?expired")
            );

        return http.build();
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
}

セキュリティチェックリスト

項目 推奨設定 理由
セッション固定攻撃対策 changeSessionId() 認証時にセッションIDを変更
セッションCookieのHttpOnly true JavaScriptからのアクセスを防止
セッションCookieのSecure true(本番) HTTPS通信のみでCookieを送信
セッションCookieのSameSite LaxまたはStrict CSRF攻撃を緩和
セッションタイムアウト 15〜30分 放置されたセッションのリスクを軽減
同時セッション数 1〜3 不正なアカウント共有を防止

本番環境のapplication.yml設定例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server:
  servlet:
    session:
      timeout: 30m
      cookie:
        http-only: true
        secure: true
        same-site: lax
        name: SID

spring:
  session:
    store-type: redis
    timeout: 30m
    redis:
      namespace: ${spring.application.name}:session
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}
      ssl:
        enabled: ${REDIS_SSL:false}

まとめ

本記事では、Spring Securityのセッション管理機能について解説しました。

主なポイントは以下のとおりです。

  • sessionManagement()でセッション管理の各種設定を行う
  • セッション固定攻撃対策はchangeSessionId()がデフォルトで有効
  • maximumSessions()で同時ログイン数を制限できる
  • 分散環境ではSpring Session + Redisで共有セッション管理を実現
  • セッションCookieにはHttpOnlySecureSameSiteを設定する

セッション管理は認証の根幹を担う重要な機能です。本記事で紹介した設定を適切に行い、セキュアなWebアプリケーションを構築してください。

参考リンク