# 通知設計書 2-セッションキックアウト通知

## 概要

本ドキュメントは、RuoYiシステムにおけるセッションキックアウト通知機能の設計仕様を記述したものである。同一ユーザーが複数端末からログインした場合に、設定された最大セッション数を超えた際、古いセッションを強制ログアウトさせ、ユーザーに通知を表示する機能について定義する。

### 本通知の処理概要

本通知は、セキュリティポリシーに基づく同時ログイン制御機能として、同一ユーザーが複数デバイスから同時にログインすることを制限するために使用される。maxSession設定を超えてログインが発生した場合、kickoutAfter設定に従い古いまたは新しいセッションを強制ログアウトし、「您已在别处登录，请您修改密码或重新登录」メッセージを表示する。

**業務上の目的・背景**：セキュリティ強化のため、同一アカウントの複数端末同時利用を制限する必要がある。アカウント共有の防止、不正アクセス検知、ライセンス管理（ユーザー数制限）などの業務要件を満たすために本機能が実装されている。

**通知の送信タイミング**：ユーザーがログイン中に、同一アカウントで別端末からログインが行われ、アクティブセッション数がmaxSession設定値を超えた時点で通知が発生する。被キックアウト側のセッションがHTTPリクエストを送信した際に検出される。

**通知の受信者**：キックアウト対象となったセッションのユーザー。kickoutAfter=falseの場合は先にログインしていたユーザー、kickoutAfter=trueの場合は後からログインしようとしたユーザーが受信者となる。

**通知内容の概要**：「您已在别处登录，请您修改密码或重新登录」（他の場所でログインされました。パスワードを変更するか、再度ログインしてください）というメッセージが表示される。

**期待されるアクション**：ユーザーは再度ログインを試みるか、不正アクセスの可能性がある場合はパスワードを変更する。また、管理者への報告や、自身の他端末でのログイン状況を確認することが期待される。

## 通知種別

システム内通知（HTTPレスポンス / リダイレクト）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（HTTPリクエスト処理時に即時判定・通知） |
| 優先度 | 高（セキュリティ関連） |
| リトライ | 無（即時通知のため） |

### 送信先決定ロジック

1. ユーザーログイン時にセッションIDをキャッシュ（Deque構造）に格納
2. セッション数がmaxSessionを超えた場合、kickoutAfter設定に基づきキックアウト対象を決定
   - `kickoutAfter=false`（デフォルト）：古いセッション（Dequeの末尾）をキックアウト
   - `kickoutAfter=true`：新しいセッション（Dequeの先頭）をキックアウト
3. キックアウト対象セッションに`kickout=true`属性を設定
4. 該当セッションの次回HTTPリクエスト時にキックアウト通知を表示

## 通知テンプレート

### Ajaxリクエストの場合

| 項目 | 内容 |
|-----|------|
| レスポンス形式 | JSON |
| HTTPステータス | 200（ボディにエラー情報） |

### 本文テンプレート（Ajax）

```json
{
  "code": 500,
  "msg": "您已在别处登录，请您修改密码或重新登录"
}
```

### 通常リクエストの場合

| 項目 | 内容 |
|-----|------|
| レスポンス形式 | リダイレクト |
| リダイレクト先 | kickoutUrl（設定値） |

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| - | - | - | 添付ファイルなし |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| msg | エラーメッセージ | 固定文字列 | Yes |
| code | エラーコード | 固定値（500） | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| フィルター処理 | HTTPリクエスト | session.getAttribute("kickout") == true | キックアウトフラグが立っているセッションからのリクエスト |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| maxSession == -1 | 最大セッション数が-1の場合、制限なし |
| 未認証状態 | ログインしていないユーザーは対象外 |
| セッション数 <= maxSession | 最大セッション数以内であれば通知なし |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[HTTPリクエスト受信] --> B{認証済み?}
    B -->|No| C[次のフィルターへ]
    B -->|Yes| D{maxSession == -1?}
    D -->|Yes| C
    D -->|No| E[セッションIDをキャッシュに追加]
    E --> F{セッション数 > maxSession?}
    F -->|No| G{kickout属性 == true?}
    F -->|Yes| H[キックアウト対象決定]
    H --> I[対象セッションにkickout=true設定]
    I --> G
    G -->|No| C
    G -->|Yes| J[subject.logout]
    J --> K{Ajaxリクエスト?}
    K -->|Yes| L[JSONエラーレスポンス]
    K -->|No| M[kickoutUrlへリダイレクト]
    L --> N[処理終了]
    M --> N
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| - | - | データベースは直接参照しない（キャッシュ使用） |

### キャッシュ参照

| キャッシュ名 | 用途 | 構造 |
|------------|------|------|
| sys-usercache | ユーザーごとのセッションID管理 | Map<String, Deque<Serializable>> |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| - | - | データベースは直接更新しない |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| セッション取得失敗 | セッションが無効化されている | エラー無視、リダイレクト |
| キャッシュアクセス失敗 | キャッシュ接続エラー | エラー無視、処理続行 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 0回（即時通知のため不要） |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし（リクエストごとに判定） |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし。24時間いつでもキックアウト判定が行われる。

## セキュリティ考慮事項

- **セッションハイジャック対策**：同一アカウントの複数ログインを検知することで、セッションハイジャックの可能性を通知
- **パスワード変更推奨**：通知メッセージでパスワード変更を促すことで、セキュリティ意識向上
- **キャッシュベース制御**：セッション情報をキャッシュで管理し、データベース負荷を軽減
- **ログアウト処理**：キックアウト時に適切にsubject.logout()を呼び出し、セッションを無効化

## 備考

- maxSession設定はShiro設定ファイルで定義
- kickoutUrl設定でキックアウト後のリダイレクト先を指定可能
- kickoutAfter設定で先/後ログインのどちらをキックアウトするか選択可能
- キャッシュ名「sys-usercache」はehcache設定と一致している必要がある

---

## コードリーディングガイド

本通知を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

セッション管理に使用されるキャッシュ構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | ShiroConstants.java | `ruoyi-common/src/main/java/com/ruoyi/common/constant/ShiroConstants.java` | キャッシュ名定義（SYS_USERCACHE） |

**読解のコツ**: キャッシュのキー（ログイン名）とバリュー（セッションIDのDeque）の関係を理解する。

#### Step 2: エントリーポイントを理解する

キックアウトフィルターの処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | KickoutSessionFilter.java | `ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/kickout/KickoutSessionFilter.java` | フィルター処理全体、キックアウトロジック |

**主要処理フロー**:
1. **37-48行目**: 設定フィールド（maxSession, kickoutAfter, kickoutUrl）
2. **53-58行目**: isAccessAllowed - 常にfalseを返し、onAccessDeniedで処理
3. **61-132行目**: onAccessDenied - メインのキックアウト処理
4. **78-92行目**: セッションIDのDeque管理
5. **95-116行目**: maxSession超過時のキックアウト対象決定
6. **119-125行目**: kickout属性チェックとログアウト処理
7. **134-148行目**: isAjaxResponse - Ajax/通常リクエストの判定とレスポンス

#### Step 3: レスポンス生成を理解する

Ajax/通常リクエストでのレスポンス生成を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | AjaxResult.java | `ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java` | Ajaxレスポンス構造 |
| 3-2 | ServletUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java` | Ajaxリクエスト判定、レスポンス出力 |

**主要処理フロー**:
- **140行目**: `AjaxResult.error("您已在别处登录，请您修改密码或重新登录")` - エラーメッセージ設定
- **141行目**: `ServletUtils.renderString()` - JSONレスポンス出力
- **145行目**: `WebUtils.issueRedirect()` - 通常リクエストのリダイレクト

### プログラム呼び出し階層図

```
ShiroFilterFactoryBean (フィルターチェーン設定)
    │
    └─ KickoutSessionFilter (キックアウトフィルター)
           │
           ├─ Cache<String, Deque<Serializable>> (セッションキャッシュ)
           │      └─ CacheManager (Ehcache)
           │
           ├─ SessionManager (セッション管理)
           │      └─ Session.getAttribute("kickout")
           │
           ├─ ShiroUtils.getSysUser() (ユーザー情報取得)
           │
           ├─ ServletUtils.isAjaxRequest() (リクエスト種別判定)
           │
           └─ Subject.logout() (ログアウト処理)
```

### データフロー図

```
[入力]               [処理]                    [出力]

HTTPリクエスト ───▶ KickoutSessionFilter ───▶ レスポンス
    │                    │                       │
    │                    ▼                       ├─ JSON (Ajax)
    │               キャッシュ参照               │
    │                    │                       └─ リダイレクト (通常)
    │                    ▼
    └───────────▶ セッション状態確認
                         │
                         ▼
                 kickout判定・ログアウト
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| KickoutSessionFilter.java | `ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/kickout/KickoutSessionFilter.java` | ソース | キックアウトフィルター実装 |
| ShiroConstants.java | `ruoyi-common/src/main/java/com/ruoyi/common/constant/ShiroConstants.java` | ソース | Shiro定数定義 |
| AjaxResult.java | `ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java` | ソース | Ajaxレスポンス構造 |
| ServletUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java` | ソース | サーブレットユーティリティ |
| ShiroUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/ShiroUtils.java` | ソース | Shiroユーティリティ |
| ShiroConfig.java | `ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java` | 設定 | Shiro設定クラス |
| ehcache-shiro.xml | `ruoyi-admin/src/main/resources/ehcache/ehcache-shiro.xml` | 設定 | Ehcacheキャッシュ設定 |
