第7章: セッションとスコープ
Webアプリケーションにおけるセッション管理とスコープ制御
この章で学ぶこと
- Webアプリケーションにおけるスコープの概念と種類
- セッション管理の基本とライフサイクルの理解
- セッション属性の設定・取得・削除方法
- ログイン認証システムの実装技術
- セッション管理のセキュリティ対策と最適化手法
7.1 Webアプリケーションにおけるスコープ
スコープ(scope)は、変数やオブジェクトがアクセス可能な範囲と生存期間を定義する重要な概念です。JSPでは、4つの標準スコープがあり、それぞれ異なる用途と特性を持ちます。
flowchart TD
A[JSPスコープ階層] --> B[pageスコープ]
A --> C[requestスコープ]
A --> D[sessionスコープ]
A --> E[applicationスコープ]
B --> F["現在ページ
(PageContext)"] C --> G["単一リクエスト
(HttpServletRequest)"] D --> H["ユーザーセッション
(HttpSession)"] E --> I["アプリケーション全体
(ServletContext)"] F --> J[最短生存期間
ページ処理中のみ] G --> K[リクエスト処理中
forward先まで有効] H --> L[セッション継続中
ユーザー固有] I --> M[アプリケーション稼働中
全ユーザー共有]
(PageContext)"] C --> G["単一リクエスト
(HttpServletRequest)"] D --> H["ユーザーセッション
(HttpSession)"] E --> I["アプリケーション全体
(ServletContext)"] F --> J[最短生存期間
ページ処理中のみ] G --> K[リクエスト処理中
forward先まで有効] H --> L[セッション継続中
ユーザー固有] I --> M[アプリケーション稼働中
全ユーザー共有]
各スコープの詳細
1. pageスコープ(PageContext)
- 生存期間: 現在のJSPページの処理中のみ
- アクセス方法:
pageContext.getAttribute()
- 用途: 一時的な計算結果、ページ内でのみ使用する変数
- 特徴: 最も狭いスコープで、メモリ使用量が最小
2. requestスコープ(HttpServletRequest)
- 生存期間: HTTPリクエストの処理が完了するまで
- アクセス方法:
request.getAttribute()
- 用途: フォーム処理結果、エラーメッセージ、画面間データ受け渡し
- 特徴: forward先のページでもアクセス可能
3. sessionスコープ(HttpSession)
- 生存期間: ユーザーセッションが継続している間
- アクセス方法:
session.getAttribute()
- 用途: ログイン情報、ユーザー設定、ショッピングカート
- 特徴: ユーザー固有データの永続化
4. applicationスコープ(ServletContext)
- 生存期間: Webアプリケーションが稼働している間
- アクセス方法:
application.getAttribute()
- 用途: 設定情報、カウンタ、キャッシュデータ
- 特徴: 全ユーザーで共有されるグローバルデータ
7.2 セッション管理の仕組み
HTTPはステートレスプロトコルのため、サーバーは個々のリクエストを独立して処理します。セッション管理は、複数のリクエストにわたってユーザーの状態を維持するメカニズムです。
sequenceDiagram
participant Client as クライアント
(ブラウザ) participant Server as Webサーバー
(Tomcat) participant Session as セッション
ストレージ Note over Client,Server: 初回アクセス時 Client->>Server: HTTP リクエスト送信 Server->>Session: 新しいセッション作成 Session-->>Server: セッションID生成 Server->>Client: レスポンス + Set-Cookie(JSESSIONID) Note over Client,Server: 2回目以降のアクセス Client->>Server: HTTP リクエスト + Cookie(JSESSIONID) Server->>Session: セッションID でユーザー特定 Session-->>Server: 保存済みデータ取得 Server->>Client: ユーザー固有レスポンス
(ブラウザ) participant Server as Webサーバー
(Tomcat) participant Session as セッション
ストレージ Note over Client,Server: 初回アクセス時 Client->>Server: HTTP リクエスト送信 Server->>Session: 新しいセッション作成 Session-->>Server: セッションID生成 Server->>Client: レスポンス + Set-Cookie(JSESSIONID) Note over Client,Server: 2回目以降のアクセス Client->>Server: HTTP リクエスト + Cookie(JSESSIONID) Server->>Session: セッションID でユーザー特定 Session-->>Server: 保存済みデータ取得 Server->>Client: ユーザー固有レスポンス
セッション管理の実装方式
1. Cookieベース(デフォルト)
- セッションIDをクッキーで管理(JSESSIONID)
- 最も一般的で効率的な方式
- クッキーが無効化されている場合は動作しない
2. URLリライティング
- すべてのURLにセッションIDを付加
- クッキー無効時の代替手段
- URLが冗長になりセキュリティ上のリスクあり
実習 7-1: セッション基本操作の実装
セッションの作成、属性の設定・取得・削除を実装し、セッション情報を管理するシステムを作成します。
手順1: セッション管理ページ(sessionManager.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="java.util.*, java.text.*" %>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>セッション管理システム</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.session-info { background-color: #e3f2fd; padding: 15px; margin: 10px 0; border-radius: 5px; }
.attributes-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
.attributes-table th, .attributes-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.attributes-table th { background-color: #f57c00; color: white; }
.form-section { background-color: #f5f5f5; padding: 15px; margin: 15px 0; border-radius: 5px; }
.button { background-color: #f57c00; color: white; padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; margin: 5px; }
.danger-button { background-color: #f44336; }
.success { background-color: #e8f5e8; color: #2e7d32; padding: 10px; margin: 10px 0; border-radius: 4px; }
.warning { background-color: #fff3e0; color: #ef6c00; padding: 10px; margin: 10px 0; border-radius: 4px; }
</style>
</head>
<body>
<h1>セッション管理システム</h1>
<%
// リクエストパラメータの処理
String action = request.getParameter("action");
String key = request.getParameter("key");
String value = request.getParameter("value");
String message = "";
// アクションに応じた処理
if ("add".equals(action) && key != null && !key.trim().isEmpty()) {
session.setAttribute(key, value != null ? value : "");
message = "属性 '" + key + "' を追加しました。";
} else if ("remove".equals(action) && key != null) {
Object removedValue = session.getAttribute(key);
session.removeAttribute(key);
message = "属性 '" + key + "' を削除しました。(値: " + removedValue + ")";
} else if ("clear".equals(action)) {
session.invalidate();
session = request.getSession(true); // 新しいセッションを作成
message = "セッションをクリアしました。新しいセッションが開始されました。";
}
// セッション情報の取得
String sessionId = session.getId();
boolean isNew = session.isNew();
long creationTime = session.getCreationTime();
long lastAccessTime = session.getLastAccessedTime();
int maxInactiveInterval = session.getMaxInactiveInterval();
// 属性一覧の取得
Enumeration<String> attributeNames = session.getAttributeNames();
Map<String, Object> attributes = new HashMap<>();
while (attributeNames.hasMoreElements()) {
String name = attributeNames.nextElement();
attributes.put(name, session.getAttribute(name));
}
// 日付フォーマット
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
%>
<%-- メッセージ表示 --%>
<% if (!message.isEmpty()) { %>
<div class="success"><%= message %></div>
<% } %>
<%-- セッション基本情報 --%>
<div class="session-info">
<h3>セッション基本情報</h3>
<table class="attributes-table">
<tr><th>項目</th><th>値</th><th>説明</th></tr>
<tr>
<td>セッションID</td>
<td><%= sessionId %></td>
<td>ユーザーを一意に識別するID</td>
</tr>
<tr>
<td>新規セッション</td>
<td><%= isNew ? "はい" : "いいえ" %></td>
<td>今回のリクエストで作成されたか</td>
</tr>
<tr>
<td>作成時刻</td>
<td><%= dateFormat.format(new Date(creationTime)) %></td>
<td>セッション開始時刻</td>
</tr>
<tr>
<td>最終アクセス時刻</td>
<td><%= dateFormat.format(new Date(lastAccessTime)) %></td>
<td>前回のリクエスト時刻</td>
</tr>
<tr>
<td>有効期限</td>
<td><%= maxInactiveInterval %>秒(<%= maxInactiveInterval/60 %>分)</td>
<td>無操作でセッションが破棄される時間</td>
</tr>
</table>
</div>
<%-- セッション属性一覧 --%>
<div>
<h3>セッション属性一覧(<%= attributes.size() %>件)</h3>
<% if (attributes.isEmpty()) { %>
<div class="warning">現在、セッションに保存されている属性はありません。</div>
<% } else { %>
<table class="attributes-table">
<tr><th>キー</th><th>値</th><th>型</th><th>操作</th></tr>
<% for (Map.Entry<String, Object> entry : attributes.entrySet()) { %>
<tr>
<td><%= entry.getKey() %></td>
<td><%= entry.getValue() %></td>
<td><%= entry.getValue().getClass().getSimpleName() %></td>
<td>
<form method="post" style="display:inline;">
<input type="hidden" name="action" value="remove">
<input type="hidden" name="key" value="<%= entry.getKey() %>">
<button type="submit" class="button danger-button" onclick="return confirm('属性を削除しますか?')">削除</button>
</form>
</td>
</tr>
<% } %>
</table>
<% } %>
</div>
<%-- セッション属性追加フォーム --%>
<div class="form-section">
<h3>セッション属性の追加</h3>
<form method="post">
<input type="hidden" name="action" value="add">
<table>
<tr>
<td>キー:</td>
<td><input type="text" name="key" required placeholder="例: username"></td>
</tr>
<tr>
<td>値:</td>
<td><input type="text" name="value" placeholder="例: 田中太郎"></td>
</tr>
<tr>
<td colspan="2">
<button type="submit" class="button">追加</button>
</td>
</tr>
</table>
</form>
</div>
<%-- サンプルデータ設定 --%>
<div class="form-section">
<h3>サンプルデータの設定</h3>
<form method="post">
<input type="hidden" name="action" value="add">
<input type="hidden" name="key" value="sampleUser">
<input type="hidden" name="value" value="田中太郎">
<button type="submit" class="button">ユーザー名を設定</button>
</form>
<form method="post" style="display:inline;">
<input type="hidden" name="action" value="add">
<input type="hidden" name="key" value="loginTime">
<input type="hidden" name="value" value="<%= new Date() %>">
<button type="submit" class="button">ログイン時刻を設定</button>
</form>
<form method="post" style="display:inline;">
<input type="hidden" name="action" value="add">
<input type="hidden" name="key" value="accessCount">
<input type="hidden" name="value" value="1">
<button type="submit" class="button">アクセス数を設定</button>
</form>
</div>
<%-- セッション操作 --%>
<div class="form-section">
<h3>セッション操作</h3>
<form method="post" style="display:inline;">
<input type="hidden" name="action" value="clear">
<button type="submit" class="button danger-button" onclick="return confirm('セッションを完全にクリアしますか?')">セッションクリア</button>
</form>
<button type="button" class="button" onclick="location.reload()">ページ更新</button>
<button type="button" class="button" onclick="window.open('sessionManager.jsp', '_blank')">新しいタブで開く</button>
</div>
<div style="margin-top: 20px; padding: 10px; background-color: #f0f0f0; border-radius: 5px;">
<h4>テスト手順</h4>
<ol>
<li>サンプルデータを設定してセッション属性を確認</li>
<li>ページを更新してセッションが保持されることを確認</li>
<li>新しいタブで開いて同じセッションが共有されることを確認</li>
<li>ブラウザを閉じて再度アクセスし、セッションが変わることを確認</li>
</ol>
</div>
</body>
</html>
期待される結果
セッションの基本情報表示、属性の追加・削除・一覧表示が正常に動作し、セッションの動作原理を理解できます。
7.3 セッションを活用したログイン認証システム
実際のWebアプリケーションでは、セッションを使用してユーザーのログイン状態を管理します。認証システムの基本的な実装パターンを学習します。
flowchart TD
A[ログインページアクセス] --> B{ログイン済み?}
B -->|No| C[ログインフォーム表示]
B -->|Yes| D[ダッシュボード表示]
C --> E[認証情報入力]
E --> F[認証処理]
F --> G{認証成功?}
G -->|No| H[エラーメッセージ表示]
H --> C
G -->|Yes| I[セッションにユーザー情報保存]
I --> J[ダッシュボードにリダイレクト]
J --> D
D --> K[ログアウト]
K --> L[セッション無効化]
L --> M[ログインページにリダイレクト]
M --> A
実習 7-2: ログイン認証システムの実装
セッションを使用した完全なログイン認証システムを実装します。
手順1: ユーザー情報JavaBeans(LoginUser.java)
package com.example.beans;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* ログインユーザー情報を管理するJavaBeansクラス
*/
public class LoginUser implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private String username;
private String email;
private String role;
private LocalDateTime loginTime;
private String lastAccessPage;
private int accessCount;
public LoginUser() {}
public LoginUser(String userId, String username, String email, String role) {
this.userId = userId;
this.username = username;
this.email = email;
this.role = role;
this.loginTime = LocalDateTime.now();
this.accessCount = 0;
}
// getter/setter メソッド
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public LocalDateTime getLoginTime() { return loginTime; }
public void setLoginTime(LocalDateTime loginTime) { this.loginTime = loginTime; }
public String getLastAccessPage() { return lastAccessPage; }
public void setLastAccessPage(String lastAccessPage) { this.lastAccessPage = lastAccessPage; }
public int getAccessCount() { return accessCount; }
public void setAccessCount(int accessCount) { this.accessCount = accessCount; }
// ビジネスメソッド
public void incrementAccessCount() {
this.accessCount++;
}
public String getFormattedLoginTime() {
if (loginTime != null) {
return loginTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
return "";
}
public boolean isAdmin() {
return "admin".equals(role);
}
@Override
public String toString() {
return "LoginUser{userId='" + userId + "', username='" + username +
"', role='" + role + "', loginTime=" + loginTime + "}";
}
}
手順2: ログインページ(login.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="com.example.beans.LoginUser" %>
<%
// すでにログインしている場合はダッシュボードにリダイレクト
LoginUser loginUser = (LoginUser) session.getAttribute("loginUser");
if (loginUser != null) {
response.sendRedirect("dashboard.jsp");
return;
}
// エラーメッセージの取得
String errorMessage = (String) request.getAttribute("errorMessage");
%>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ログイン - セッション管理デモ</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px; }
.login-container { max-width: 400px; margin: 50px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; font-weight: bold; color: #333; }
input[type="text"], input[type="password"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
.login-button { width: 100%; padding: 12px; background-color: #f57c00; color: white; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; }
.login-button:hover { background-color: #ef6c00; }
.error-message { background-color: #ffebee; color: #c62828; padding: 10px; margin-bottom: 20px; border-radius: 4px; border: 1px solid #ef9a9a; }
.demo-accounts { background-color: #e3f2fd; padding: 15px; margin-top: 20px; border-radius: 4px; }
.demo-accounts h4 { margin-top: 0; color: #1976d2; }
</style>
</head>
<body>
<div class="login-container">
<h2 style="text-align: center; color: #333; margin-bottom: 30px;">ユーザーログイン</h2>
<%-- エラーメッセージの表示 --%>
<% if (errorMessage != null) { %>
<div class="error-message">
<%= errorMessage %>
</div>
<% } %>
<form action="loginProcess.jsp" method="post">
<div class="form-group">
<label for="userId">ユーザーID:</label>
<input type="text" id="userId" name="userId" required>
</div>
<div class="form-group">
<label for="password">パスワード:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="login-button">ログイン</button>
</form>
<div class="demo-accounts">
<h4>デモアカウント</h4>
<p><strong>管理者:</strong> ID: admin / パスワード: admin123</p>
<p><strong>一般ユーザー:</strong> ID: user / パスワード: user123</p>
<p><strong>ゲスト:</strong> ID: guest / パスワード: guest123</p>
</div>
</div>
</body>
</html>
手順3: ログイン処理(loginProcess.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="com.example.beans.LoginUser" %>
<%
// 文字エンコーディング設定
request.setCharacterEncoding("UTF-8");
// パラメータ取得
String userId = request.getParameter("userId");
String password = request.getParameter("password");
// 簡易認証データベース(実際の開発ではデータベースを使用)
boolean isAuthenticated = false;
String username = "";
String email = "";
String role = "user";
if ("admin".equals(userId) && "admin123".equals(password)) {
isAuthenticated = true;
username = "管理者";
email = "admin@example.com";
role = "admin";
} else if ("user".equals(userId) && "user123".equals(password)) {
isAuthenticated = true;
username = "一般ユーザー";
email = "user@example.com";
role = "user";
} else if ("guest".equals(userId) && "guest123".equals(password)) {
isAuthenticated = true;
username = "ゲストユーザー";
email = "guest@example.com";
role = "guest";
}
if (isAuthenticated) {
// 認証成功:ユーザー情報をセッションに保存
LoginUser loginUser = new LoginUser(userId, username, email, role);
session.setAttribute("loginUser", loginUser);
// セキュリティ強化:セッション固定攻撃対策
// セッションIDを再生成(実際の本番環境では推奨)
// session.invalidate();
// session = request.getSession(true);
// session.setAttribute("loginUser", loginUser);
// ログイン成功メッセージ
session.setAttribute("successMessage", username + "さん、ログインしました。");
// ダッシュボードページにリダイレクト
response.sendRedirect("dashboard.jsp");
} else {
// 認証失敗:エラーメッセージを設定してログインページに戻る
request.setAttribute("errorMessage", "ユーザーIDまたはパスワードが正しくありません。");
// ログインページにforward
RequestDispatcher dispatcher = request.getRequestDispatcher("login.jsp");
dispatcher.forward(request, response);
}
%>
手順4: ダッシュボードページ(dashboard.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="com.example.beans.LoginUser" %>
<%
// ログイン状態の確認
LoginUser loginUser = (LoginUser) session.getAttribute("loginUser");
if (loginUser == null) {
// 未ログインの場合はログインページにリダイレクト
response.sendRedirect("login.jsp");
return;
}
// アクセス数を増やす
loginUser.incrementAccessCount();
loginUser.setLastAccessPage("dashboard.jsp");
// 成功メッセージの取得と削除
String successMessage = (String) session.getAttribute("successMessage");
if (successMessage != null) {
session.removeAttribute("successMessage");
}
%>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ダッシュボード - セッション管理デモ</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
.header { background-color: #f57c00; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center; }
.content { max-width: 1000px; margin: 20px auto; padding: 20px; }
.welcome-section { background: white; padding: 20px; margin-bottom: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
.info-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.button { background-color: #f57c00; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; margin: 5px; }
.button.danger { background-color: #f44336; }
.button:hover { opacity: 0.9; }
.success-message { background-color: #e8f5e8; color: #2e7d32; padding: 10px; margin-bottom: 20px; border-radius: 4px; }
.admin-section { background-color: #fff3e0; border: 2px solid #f57c00; border-radius: 8px; padding: 15px; margin-top: 20px; }
</style>
</head>
<body>
<div class="header">
<h1>ダッシュボード</h1>
<div>
<span>ようこそ、<%= loginUser.getUsername() %>さん</span>
<a href="logout.jsp" class="button danger">ログアウト</a>
</div>
</div>
<div class="content">
<%-- 成功メッセージの表示 --%>
<% if (successMessage != null) { %>
<div class="success-message"><%= successMessage %></div>
<% } %>
<div class="welcome-section">
<h2><%= loginUser.getUsername() %>さんのダッシュボード</h2>
<p>セッション管理システムへようこそ。現在ログイン中です。</p>
</div>
<div class="info-grid">
<div class="info-card">
<h3>ユーザー情報</h3>
<table>
<tr><td><strong>ユーザーID:</strong></td><td><%= loginUser.getUserId() %></td></tr>
<tr><td><strong>ユーザー名:</strong></td><td><%= loginUser.getUsername() %></td></tr>
<tr><td><strong>メールアドレス:</strong></td><td><%= loginUser.getEmail() %></td></tr>
<tr><td><strong>役割:</strong></td><td><%= loginUser.getRole() %></td></tr>
</table>
</div>
<div class="info-card">
<h3>セッション情報</h3>
<table>
<tr><td><strong>ログイン時刻:</strong></td><td><%= loginUser.getFormattedLoginTime() %></td></tr>
<tr><td><strong>アクセス回数:</strong></td><td><%= loginUser.getAccessCount() %>回</td></tr>
<tr><td><strong>最終アクセスページ:</strong></td><td><%= loginUser.getLastAccessPage() %></td></tr>
<tr><td><strong>セッションID:</strong></td><td><%= session.getId() %></td></tr>
</table>
</div>
</div>
<div class="info-card">
<h3>利用可能な機能</h3>
<a href="profile.jsp" class="button">プロフィール</a>
<a href="sessionManager.jsp" class="button">セッション詳細</a>
<a href="dashboard.jsp" class="button">ダッシュボード更新</a>
<%-- 管理者のみに表示される機能 --%>
<% if (loginUser.isAdmin()) { %>
<div class="admin-section">
<h4>管理者専用機能</h4>
<p>あなたは管理者権限でログインしています。</p>
<a href="adminPanel.jsp" class="button">管理者パネル</a>
</div>
<% } %>
</div>
<div class="info-card">
<h3>テスト項目</h3>
<ul>
<li>ページを更新してアクセス数が増加することを確認</li>
<li>新しいタブで開いて同じセッション情報が表示されることを確認</li>
<li>ログアウトしてセッションが破棄されることを確認</li>
<li>直接URLでアクセスしてログインページにリダイレクトされることを確認</li>
</ul>
</div>
</div>
</body>
</html>
手順5: ログアウト処理(logout.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="com.example.beans.LoginUser" %>
<%
// ログイン情報の取得(ログアウトメッセージ用)
LoginUser loginUser = (LoginUser) session.getAttribute("loginUser");
String username = "ユーザー";
if (loginUser != null) {
username = loginUser.getUsername();
}
// セッションの完全な破棄
session.invalidate();
// 新しいセッションを開始してメッセージを保存
HttpSession newSession = request.getSession(true);
newSession.setAttribute("logoutMessage", username + "さん、ログアウトしました。");
// ログインページにリダイレクト
response.sendRedirect("login.jsp");
%>
期待される結果
- 適切な認証情報でログイン成功
- セッション情報の保持とアクセス数の増加
- 未ログイン時の自動リダイレクト
- ログアウト処理によるセッション破棄
7.4 セッション管理のセキュリティ対策
Webアプリケーションのセッション管理には、様々なセキュリティリスクが存在します。適切な対策を実装することで、安全なセッション管理を実現できます。
主なセキュリティリスク
- セッション固定攻撃: 攻撃者が事前に用意したセッションIDを強制使用
- セッションハイジャック: セッションIDの盗取による成りすまし
- セッション予測攻撃: 推測可能なセッションIDによる攻撃
- クロスサイトスクリプティング(XSS): セッション情報の漏えい
- 中間者攻撃: 通信経路でのセッションID傍受
セキュリティ対策の実装
1. セッションIDの再生成
// ログイン成功時にセッションIDを再生成(セッション固定攻撃対策)
// 古いセッションを無効化
session.invalidate();
// 新しいセッションを作成
HttpSession newSession = request.getSession(true);
// ユーザー情報を新しいセッションに設定
newSession.setAttribute("loginUser", loginUser);
2. セッションタイムアウトの設定
// セッション有効期間を30分に設定
session.setMaxInactiveInterval(30 * 60); // 秒単位
// web.xmlでの設定例
/*
<session-config>
<session-timeout>30</session-timeout>
</session-config>
*/
3. Cookieの安全な設定
// HTTPSでのみ送信(Secure属性)
response.setHeader("Set-Cookie", "JSESSIONID=" + session.getId() + "; Secure; HttpOnly; SameSite=Strict");
// web.xmlでの設定例
/*
<session-config>
<cookie-config>
<secure>true</secure>
<http-only>true</http-only>
<same-site>strict</same-site>
</cookie-config>
</session-config>
*/
4. セッション状態の検証
// IPアドレスの検証(セッションハイジャック対策)
String sessionIP = (String) session.getAttribute("clientIP");
String currentIP = request.getRemoteAddr();
if (sessionIP == null) {
session.setAttribute("clientIP", currentIP);
} else if (!sessionIP.equals(currentIP)) {
// IPアドレスが変更された場合はセッションを無効化
session.invalidate();
response.sendRedirect("login.jsp?error=session_invalid");
return;
}
7.5 セッション管理の最適化
大規模なWebアプリケーションでは、セッション管理のパフォーマンスと拡張性を考慮した設計が重要です。
最適化のポイント
1. セッションデータの最小化
- 必要最小限の情報のみセッションに保存
- 大きなオブジェクトはデータベースに保存し、IDのみセッション管理
- 不要になった属性は即座に削除
2. セッションストレージの選択
- メモリ: 高速だが、サーバー再起動で消失
- データベース: 永続化されるが、パフォーマンス低下
- Redis/Memcached: 高速で永続化も可能(推奨)
3. セッション監視とメンテナンス
// セッション作成/破棄を監視するリスナー
public class SessionListener implements HttpSessionListener {
private static int activeSessions = 0;
public void sessionCreated(HttpSessionEvent se) {
activeSessions++;
System.out.println("セッション作成. アクティブセッション数: " + activeSessions);
}
public void sessionDestroyed(HttpSessionEvent se) {
activeSessions--;
System.out.println("セッション破棄. アクティブセッション数: " + activeSessions);
}
public static int getActiveSessions() {
return activeSessions;
}
}
理解度確認クイズ
-
スコープの生存期間について
以下の各スコープについて、データが保持される期間を説明してください:- pageスコープ
- requestスコープ
- sessionスコープ
- applicationスコープ
-
セッション管理の仕組み
HTTPがステートレスプロトコルであるにも関わらず、Webアプリケーションでユーザーの状態を保持できる理由を説明してください。 -
セキュリティ対策
セッション固定攻撃とは何か、またその対策方法を答えてください。 -
実装の判断
以下の情報を保存する際、どのスコープを使用すべきか理由とともに答えてください:- フォーム入力エラーの一覧
- ショッピングカートの内容
- サイト全体の訪問者数
- 検索結果の一時データ
-
パフォーマンス最適化
セッションストレージとして、メモリ、データベース、Redis を使用する場合のメリット・デメリットをそれぞれ挙げてください。
7.6 まとめ
この章では、JSPにおけるセッションとスコープ管理について包括的に学習しました。
重要なポイント
- スコープの理解: 4つのスコープの特性と適切な使い分け
- セッション管理: HTTPのステートレス性を克服する仕組み
- 認証システム: セッションを活用したログイン状態の管理
- セキュリティ対策: セッション固定攻撃などからの防護
- 最適化技術: パフォーマンスと拡張性を考慮した設計