システムアーキテクチャと技術スタック
本システムは、Spring BootによるRESTful APIバックエンドと、Vue.jsおよびuni-appで構築されたクライアント側を組み合わせ、映画館のチケット予約および座席指定機能を実現する。データアクセス層にはMyBatis-Plusを採用し、クエリ構築の効率化とメンテナンス性の向上を図っている。
バックエンド:Spring Boot
TomcatやUndertowなどの組込みWebサーバーを標準搭載し、外部アプリケーションサーバーの依存を排除する。依存関係の自動検出と設定(Auto-configuration)により、初期構成コードを最小化し、Spring SecurityやSpring Dataとの連携によるセキュアなエンドポイント設計を支援する。プラグインエコシステムを活用することで、スケーラビリティと拡張性を確保している。
フロントエンド:Vue.js & uni-app
仮想DOMによる差分レンダリングと、リアクティブなデータバインディング機構を採用。状態変更が自動的にUIに反映されるため、座席選択グリッドの描画パフォーマンスや予約フォームの状態管理が最適化される。コンポーネントベースの設計により、コードの再利用性と保守性を確保し、マルチプラットフォーム(WeChat Mini Programなど)へのスムーズな移植を可能にする。
データアクセス層:MyBatis-Plus
MyBatisの拡張として提供される高生産性ORMツール。基本的なCRUD操作の自動生成、動的SQL構築、ページネーションサポート、および楽観的ロック機能を内蔵。コードジェネレーターを活用することで、エンティティクラスとマッパーインタフェースの定義作業を自動化し、開発サイクルを短縮する。
認証機構とセッション管理の実装
システムへのアクセス制御は、独自トークンベースのセッション検証により実現される。ログインエンドポイントは入力資格情報を検証し、有効期限付きのアクセスキーを生成して返却する。各リクエストではインターセプタがヘッダー情報を検査し、認可が確認された場合のみリクエストコンテキストにユーザー情報をバインドする。
@RestController
@RequestMapping("/v1/auth")
@RequiredArgsConstructor
public class AuthenticationEndpoint {
private final UserRepository userRepo;
private final TokenService tokenSvc;
@PostMapping("/access")
public ResponseEntity<ApiResponse> processLogin(@Valid @RequestBody LoginPayload payload) {
UserRecord user = userRepo.fetchByIdentifier(payload.getAccountName())
.orElseThrow(() -> new UnauthorizedException("認証情報が登録されていません"));
if (!SecurityUtil.verify(payload.getCredential(), user.getEncryptedSecret())) {
return ResponseEntity.status(400).body(ApiResponse.reject("認証に失敗しました"));
}
String issuedKey = tokenSvc.createAccessKey(user.getId(), user.getPrivilegeLevel());
return ResponseEntity.ok(ApiResponse.success(Map.of("session_id", issuedKey)));
}
}
@Service
@RequiredArgsConstructor
public class StandardTokenService implements TokenService {
private final SessionMapper sessionMapper;
@Override
public String createAccessKey(Long uid, String level) {
String newKey = CryptoUtil.randomAlphaNumeric(36);
LocalDateTime expiry = LocalDateTime.now().plusMinutes(120);
SessionRecord current = sessionMapper.lookupActiveKey(uid, level);
if (current != null) {
sessionMapper.renewSession(current.getId(), newKey, expiry);
} else {
sessionMapper.save(new SessionRecord(uid, level, newKey, expiry));
}
return newKey;
}
}
@Component
@RequiredArgsConstructor
public class RequestGuard implements HandlerInterceptor {
private static final String AUTH_HEADER = "X-Cinema-Token";
private final TokenService tokenSvc;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpStatus.NO_CONTENT.value());
return false;
}
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600");
String token = request.getHeader(AUTH_HEADER);
if (!StringUtils.hasText(token)) {
return rejectRequest(response, 401, "認可ヘッダーが欠落しています");
}
SessionRecord session = tokenSvc.validateToken(token);
if (session == null) {
return rejectRequest(response, 401, "セッションが期限切れまたは無効です");
}
request.setAttribute("CONTEXT_USER_ID", session.getUserId());
request.setAttribute("CONTEXT_PRIVILEGE", session.getRole());
return true;
}
private boolean rejectRequest(HttpServletResponse res, int code, String message) throws IOException {
res.setStatus(code);
res.setContentType("application/json;charset=UTF-8");
res.getWriter().write("{\"status\":" + code + ",\"msg\":\"" + message + "\"}");
return false;
}
}
データベース設計(セッション管理表)
発効されたトークン情報は、MySQLのInnoDBエンジンを使用して永続化される。セッションキーはユニーク制約が適用され、有効期限管理のためにインデックスが設定されている。
CREATE TABLE `system_sessions` (
`record_id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`user_ref` BIGINT(20) NOT NULL COMMENT 'ユーザー識別子',
`account_name` VARCHAR(64) NOT NULL,
`privilege_role` VARCHAR(32) DEFAULT NULL COMMENT '権限レベル',
`access_key` VARCHAR(64) NOT NULL UNIQUE COMMENT '認証トークン',
`issued_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`valid_until` TIMESTAMP NOT NULL DEFAULT '2030-01-01 00:00:00',
PRIMARY KEY (`record_id`),
INDEX `idx_valid_until` (`valid_until`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='認証セッション格納表';
INSERT INTO `system_sessions` VALUES (1, 101, 'staff_01', 'employee', 'k8d9f2m1n4p7q3s6v9x2z5b8c1', '2024-05-10 14:20:00', '2024-05-10 16:20:00');
INSERT INTO `system_sessions` VALUES (2, 102, 'admin_root', 'superuser', 'm2p5s8v1y4b7e0h3k6n9q2t5w8', '2024-05-10 15:05:30', '2024-05-10 17:05:30');
INSERT INTO `system_sessions` VALUES (3, 103, 'manager_a', 'admin', 'x7y0z3c6f9j2m5p8r1u4w7a0', '2024-05-11 09:12:45', '2024-05-11 11:12:45');
機能検証とテスト実施
システムの安定性を確保するため、ブラックボックステスト手法に基づき、入力境界値・必須項目検証・権限チェックを中心に検証を行う。以下のケースは認証フローとユーザー管理モジュールの検証結果を示している。
ログイン認証の検証シナリオ
| 入力パラメータ | 期待動作 | 実測結果 | 検証判定 |
|---|---|---|---|
| ID: admin_01 / PW: ****** / Key: 正常 | 認証成功・トークン発行 | セッションキー返却 | 合格 |
| ID: admin_01 / PW: 誤り / Key: 正常 | 認証拒否・エラー応答 | 400ステータス・認証失敗メッセージ | 合格 |
| ID: admin_01 / PW: ****** / Key: 誤り | 検証不一致エラー | 400ステータス・検証不一致警告 | 合格 |
| ID: 空欄 / PW: ****** / Key: 正常 | 必須項目欠落警告 | 400ステータス・ID未入力提示 | 合格 |
| ID: admin_01 / PW: 空欄 / Key: 正常 | パスワード欠落警告 | 400ステータス・PW未入力提示 | 合格 |
ユーザー情報管理の検証シナリオ
| 操作パターン | 期待動作 | 実測結果 | 検証判定 |
|---|---|---|---|
| 新規登録(全項目正常) | データ永続化・一覧反映 | データベース保存確認・UIリスト更新 | 合格 |
| 既存データ編集 | 更新内容の反映 | 変更値が正常に適用され画面表示 | 合格 |
| レコード削除 | 削除確認後のデータ消去 | 削除確認ダイアログ経由で対象消去 | 合格 |
| ID未入力登録 | バリデーション阻害 | 「識別子は必須です」警告表示 | 合格 |
| 重複ID登録 | 一意性制約違反検知 | 「既に使用されています」エラー応答 | 合格 |