技術スタックの概要
バックエンド: Spring Boot
Spring Bootは、Tomcat、Jetty、Undertowなどのサーバーを内蔵しており、追加のインストールや設定なしに直接使用できます。主な利点の一つは自動構成機能です。プロジェクトの依存関係に基づいてアプリケーションを自動的に構成するため、各依存項目を手動で設定する手間が省けます。また、Spring Data、Spring Security、Spring Cloudなど、すぐに使用可能な多数の機能とプラグインを提供しています。これにより、開発者はより迅速にアプリケーションを構築し、他の技術との拡張や統合を容易に行えます。
フロントエンド: Vue.js
Vue.jsの中核は仮想DOM技術です。仮想DOMはメモリ内のデータ構造であり、効率的なDOM操作を実現します。応答式データバインディング、仮想DOM、コンポーネント化などの最新技術を採用し、柔軟で効率的、かつ保守性の高い開発モデルを提供します。データが変更されるとUIも自動的に更新されるため、開発者は手動でUIを更新するのではなく、データ処理に集中できます。
永続化レイヤー: MyBatis-Plus
MyBatis-Plusは、MyBatisフレームワークに基づく拡張ツールであり、開発を簡素化することを目的としています。MySQL、Oracle、SQL Server、PostgreSQLなど、複数のデータベースをサポートするオープンソースのJavaフレームワークです。豊富なAPIとアノテーションを提供し、シンプルな設定と使用でORM操作を実現し、手動でSQLを記述する作業量を大幅に削減します。さらに、エンティティクラス、Mapperインターフェース、XMLマッピングファイルを自動生成するコードジェネレーターも提供しています。ページネーションクエリ、動的クエリ、オプティミスティックロック、パフォーマンス分析などの実用的な機能もサポートしており、高品質なデータアクセス層コードを迅速に開発できます。
システムテスト戦略
テストの目的
システムテストは、開発サイクルにおいて不可欠であり、忍耐力が試される工程です。その重要性は、システムの品質と信頼性を保証する最終関門であり、開発プロセス全体の最終チェックにあるからです。ユーザーが使用時に問題に直面するのを防ぎ、ユーザーエクスペリエンスを向上させるために、多角的な視点からシステムが直面する可能性のある問題を考慮し、さまざまなシミュレーションシナリオを通じて欠陥を発見し解決する必要があります。テストの目標は、システムが要件定義書の仕様を満たしていることを検証し、矛盾や冲突を見つけることです。
機能テスト
システムの機能モジュールに対して、クリック、境界値の入力、必須項目と任意項目の検証などの一連のブラックボックステストを実施します。テストケースを作成し、それに従ってテストを実行します。
ログイン機能テスト: アカウントとパスワードを使用して検証を行います。データベースに保存されているデータと一致しない場合、エラーを提示します。また、ロール権限の検証も行います。
| 入力データ | 期待される結果 | 実際の結果 | 結果分析 |
|---|---|---|---|
| ユーザー名: admin, パスワード: 123456, 認証コード: 正しい | システムにログイン | ログイン成功 | 期待通り |
| ユーザー名: admin, パスワード: 111111, 認証コード: 正しい | パスワードエラー | パスワードが間違っています | 期待通り |
| ユーザー名: admin, パスワード: 123456, 認証コード: 間違い | 認証コードエラー | 認証コードが間違っています | 期待通り |
| ユーザー名: 空, パスワード: 123456, 認証コード: 正しい | ユーザー名は必須 | ユーザー名を入力してください | 期待通り |
ユーザー管理機能テスト: ユーザーの追加、編集、削除、検索機能のテスト。必須項目の未入力検証、既存ユーザー名の重複検証、削除時の確認プロンプト、編集後の画面反映などを確認します。
| 入力データ | 期待される結果 | 実際の結果 | 結果分析 |
|---|---|---|---|
| 基本情報を入力して追加 | 追加成功、リストに表示 | リストにユーザーが表示される | 期待通り |
| ユーザー情報を編集 | 編集成功、情報が更新される | ユーザー情報が更新される | 期待通り |
| ユーザー名を空にして追加 | ユーザー名は空にできません | エラーメッセージが表示される | 期待通り |
| 既存のユーザー名を入力 | 追加失敗、重複エラー | ユーザー名が重複しています | 期待通り |
テスト結論
ブラックボックステストを中心に、ユーザーの操作をシミュレートして各機能のテストケースを作成し、実行しました。これにより、システムフローの正確性が保証されます。テストはシステムをより完璧にし、可用性を高めます。すべてのシナリオがユーザーのニーズを満たしていることを確認し、機能とパフォーマンスの両面で設計要件を満たしていることが検証されました。
認証と認可の実装
以下は、Spring Bootにおけるログイン認証、トークン生成、およびリクエストインターセプタによる権限検証の実装例です。変数名とロジック構造をモダンな設計に合わせて再構築しています。
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AccountService accountService;
@Autowired
private SessionTokenService sessionTokenService;
@SkipAuth
@PostMapping("/signin")
public ResponseEntity<ApiResponse> authenticate(@RequestBody LoginRequest request) {
Account account = accountService.findByUsername(request.getUsername());
if (account == null || !account.getPassword().equals(request.getPassword())) {
return ResponseEntity.badRequest().body(ApiResponse.error("認証失敗:アカウントまたはパスワードが正しくありません"));
}
String accessToken = sessionTokenService.createSessionToken(
account.getId(),
request.getUsername(),
account.getEntityType(),
account.getRole()
);
return ResponseEntity.ok(ApiResponse.success(Map.of("accessToken", accessToken)));
}
}
@Service
public class SessionTokenService {
@Autowired
private SessionTokenMapper tokenMapper;
public String createSessionToken(Long userId, String username, String entityType, String role) {
SessionToken existingToken = tokenMapper.selectByUserAndRole(userId, role);
String newToken = UUID.randomUUID().toString().replace("-", "");
LocalDateTime now = LocalDateTime.now();
LocalDateTime expiry = now.plusHours(2);
if (existingToken != null) {
existingToken.setAccessToken(newToken);
existingToken.setExpiresAt(expiry);
tokenMapper.updateById(existingToken);
} else {
SessionToken newSession = new SessionToken(userId, username, entityType, role, newToken, now, expiry);
tokenMapper.insert(newSession);
}
return newToken;
}
public SessionToken getSessionByToken(String token) {
return tokenMapper.selectByToken(token);
}
}
@Component
public class SecurityInterceptor implements HandlerInterceptor {
private static final String HEADER_TOKEN = "X-Access-Token";
@Autowired
private SessionTokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Access-Token");
response.setHeader("Access-Control-Max-Age", "3600");
if (RequestMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return false;
}
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (handlerMethod.hasMethodAnnotation(SkipAuth.class)) {
return true;
}
}
String token = request.getHeader(HEADER_TOKEN);
if (token != null && !token.trim().isEmpty()) {
SessionToken session = tokenService.getSessionByToken(token);
if (session != null) {
request.setAttribute("currentUserId", session.getUserId());
request.setAttribute("currentRole", session.getRole());
return true;
}
}
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未認証:ログインしてください\"}");
return false;
}
}
データベース設計
セッションとトークンを管理するためのデータベーステーブル構造です。フィールド名とデータ型を最適化しています。
CREATE TABLE `user_sessions` (
`session_id` bigint NOT NULL AUTO_INCREMENT COMMENT 'セッションID',
`user_id` bigint NOT NULL COMMENT 'ユーザーID',
`account_name` varchar(100) NOT NULL COMMENT 'アカウント名',
`entity_type` varchar(100) DEFAULT NULL COMMENT 'エンティティタイプ',
`user_role` varchar(100) DEFAULT NULL COMMENT 'ユーザー権限',
`access_token` varchar(200) NOT NULL COMMENT 'アクセストークン',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成日時',
`expires_at` datetime NOT NULL COMMENT '有効期限',
PRIMARY KEY (`session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ユーザーセッション管理テーブル';
INSERT INTO `user_sessions` (`session_id`, `user_id`, `account_name`, `entity_type`, `user_role`, `access_token`, `created_at`, `expires_at`) VALUES
(1, 101, 'user_alpha', 'student', '学生', 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6', '2023-10-01 10:00:00', '2023-10-01 12:00:00'),
(2, 102, 'user_beta', 'student', '学生', 'q1w2e3r4t5y6u7i8o9p0a1s2d3f4g5h6', '2023-10-02 14:30:00', '2023-10-02 16:30:00'),
(3, 1, 'admin_root', 'admin', '管理者', 'z9x8c7v6b5n4m3l2k1j0h9g8f7d6s5a4', '2023-10-03 09:00:00', '2023-10-03 11:00:00'),
(4, 205, 'club_president', 'club', '社長', 'p9o8i7u6y5t4r3e2w1q0a9s8d7f6g5h4', '2023-10-04 11:15:00', '2023-10-04 13:15:00');