セッションベースの認証は、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
endSpring 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 <--> FSpring 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には
HttpOnly、Secure、SameSiteを設定する
セッション管理は認証の根幹を担う重要な機能です。本記事で紹介した設定を適切に行い、セキュアなWebアプリケーションを構築してください。
参考リンク#