医療管理システムの背景と目的
医療管理システムは、医療機関の運営効率化、患者の受診体験の向上、および医療リソースの最適化を目的として設計されます。高齢化社会の進展に伴い医療需要が急速に増加する中、従来の紙ベースや分散型システムによる非効率なプロセス、情報共有の遅延、リソースの未活用といった課題を解決する必要があります。デジタルトランスフォーメーション(DX)を活用し、患者情報、電子カルテ、医薬品在庫などを一元管理することで、医療スタッフ間のコラボレーションを強化し、高精度な医療サービスを提供することが可能です。
採用技術スタック
バックエンド:Spring Boot
Spring Bootは、TomcatやUndertowなどのWebサーバーを内蔵しており、複雑なデプロイメント設定を不要にします。依存関係に基づく自動構成(Auto-Configuration)機能により、ボイラープレートコードを大幅に削減できます。また、Spring Data JPAやSpring Securityなどのエコシステムとシームレスに統合できるため、スケーラブルで堅牢なRESTful APIを迅速に構築できます。
フロントエンド:Vue.js
Vue.jsは、仮想DOM(Virtual DOM)とリアクティブデータバインディングを中核としたプログレッシブフレームワークです。コンポーネント指向のアーキテクチャを採用することで、UIの再利用性と保守性が向上します。データの状態変化が自動的にDOMに反映されるため、開発者はDOM操作よりもビジネスロジックやデータフローの設計に集中できます。
データベース:MySQL
MySQLは、高い信頼性とパフォーマンスを誇るオープンソースの関係型データベース管理システム(RDBMS)です。クロスプラットフォーム対応により多様なインフラ環境にデプロイでき、トランザクション処理や複雑なクエリの実行において安定したデータ永続化を実現します。
システムテストのアプローチ
システムの品質保証において、多角的な視点からのテストは不可欠です。本システムでは、要件定義書に基づく機能検証と、ユーザー視点でのユーザビリティ確認を目的としたブラックボックステストを実施します。
機能テストの実施
主に認証機能とユーザー管理機能に対し、正常系および異常系のテストケースを設計し、境界値や必須項目のバリデーションを検証します。
| テスト対象 | 入力データ / 操作 | 期待される結果 | 検証ステータス |
|---|---|---|---|
| ログイン認証 | 正しいID/パスワード、有効なCAPTCHA | 認証成功、JWTトークン発行 | パス |
| ログイン認証 | 誤ったパスワード | 認証失敗、エラーメッセージ表示 | パス |
| ユーザー登録 | 既存のユーザー名を入力 | 登録失敗、重複エラーのアラート | パス |
| ユーザー削除 | 削除ボタンクリック、確認ダイアログで承認 | レコード削除、リストから該当ユーザーが消える | パス |
バックエンド実装:認証と認可
以下は、ユーザー認証、トークン発行、およびリクエストインターセプターによる認可制御の実装例です。セキュリティを考慮し、パスワードのハッシュ化やモダンなJavaのAPIを活用しています。
認証コントローラー
@RestController
@RequestMapping("/api/v1/auth")
public class AuthenticationController {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private TokenManager tokenManager;
@SkipAuthentication
@PostMapping("/login")
public ResponseEntity<ApiResponse> authenticateUser(@RequestBody LoginRequest loginRequest) {
Optional<UserEntity> optionalUser = userRepository.findByUsername(loginRequest.getUsername());
if (optionalUser.isEmpty() || !passwordEncoder.matches(loginRequest.getPassword(), optionalUser.get().getPassword())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error("Invalid credentials provided."));
}
UserEntity user = optionalUser.get();
String accessToken = tokenManager.issueAccessToken(user.getId(), user.getUsername(), user.getRole());
return ResponseEntity.ok(ApiResponse.success(Map.of("accessToken", accessToken)));
}
}
トークン管理サービス
@Service
public class TokenManager {
@Autowired
private AccessTokenRepository tokenRepository;
public String issueAccessToken(Long userId, String username, String role) {
Optional<AccessTokenEntity> existingToken = tokenRepository.findByUserIdAndRole(userId, role);
String newTokenValue = UUID.randomUUID().toString().replace("-", "");
Instant expirationTime = Instant.now().plus(1, ChronoUnit.HOURS);
if (existingToken.isPresent()) {
AccessTokenEntity tokenEntity = existingToken.get();
tokenEntity.setTokenValue(newTokenValue);
tokenEntity.setExpiresAt(expirationTime);
tokenRepository.save(tokenEntity);
} else {
AccessTokenEntity newTokenEntity = new AccessTokenEntity();
newTokenEntity.setUserId(userId);
newTokenEntity.setUsername(username);
newTokenEntity.setRole(role);
newTokenEntity.setTokenValue(newTokenValue);
newTokenEntity.setExpiresAt(expirationTime);
tokenRepository.save(newTokenEntity);
}
return newTokenValue;
}
public Optional<AccessTokenEntity> validateToken(String tokenValue) {
return tokenRepository.findByTokenValueAndExpiresAtAfter(tokenValue, Instant.now());
}
}
セキュリティインターセプター
@Component
public class SecurityInterceptor implements HandlerInterceptor {
private static final String AUTH_HEADER = "Authorization";
@Autowired
private TokenManager tokenManager;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
configureCorsHeaders(response, request);
if (RequestMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpStatus.OK.value());
return false;
}
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (handlerMethod.hasMethodAnnotation(SkipAuthentication.class)) {
return true;
}
}
String token = request.getHeader(AUTH_HEADER);
if (StringUtils.hasText(token)) {
Optional<AccessTokenEntity> tokenOpt = tokenManager.validateToken(token);
if (tokenOpt.isPresent()) {
AccessTokenEntity validToken = tokenOpt.get();
request.setAttribute("currentUserId", validToken.getUserId());
request.setAttribute("currentUserRole", validToken.getRole());
return true;
}
}
sendUnauthorizedResponse(response, "Authentication required. Please log in.");
return false;
}
private void configureCorsHeaders(HttpServletResponse response, HttpServletRequest request) {
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", "Authorization, Content-Type");
response.setHeader("Access-Control-Allow-Credentials", "true");
}
private void sendUnauthorizedResponse(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\":\"" + message + "\"}");
}
}
データベース設計
セッション管理とAPI認証のためのトークンテーブルのDDLおよび初期データ投入スクリプトは以下の通りです。
CREATE TABLE `access_tokens` (
`token_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
`user_id` bigint(20) NOT NULL COMMENT 'User ID',
`login_name` varchar(100) NOT NULL COMMENT 'Username',
`user_role` varchar(50) DEFAULT NULL COMMENT 'User Role',
`access_token` varchar(255) NOT NULL COMMENT 'Token Value',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time',
`expires_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'Expiration Time',
PRIMARY KEY (`token_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_token` (`access_token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Access Tokens Table';
INSERT INTO `access_tokens` (`user_id`, `login_name`, `user_role`, `access_token`, `expires_at`) VALUES
(101, 'admin_user', 'ADMIN', 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6', '2024-12-31 23:59:59'),
(102, 'doctor_smith', 'DOCTOR', 'p6o5n4m3l2k1j0i9h8g7f6e5d4c3b2a1', '2024-12-31 23:59:59'),
(103, 'nurse_jones', 'NURSE', 'q1w2e3r4t5y6u7i8o9p0a1s2d3f4g5h6', '2024-12-31 23:59:59');