システム概要と背景
現代社会においてインターネット技術は急速に発展し、各種産業への浸透が加速しています。医療分野においても、情報のデジタル化は不可欠な課題となっています。従来の紙ベースや局所的なデータ管理では、入院記録の誤入力が頻発し、効率的な統計分析が困難であり、プライバシー保護の観点からもリスクが存在しました。これらの課題を解決するため、統合された情報管理プラットフォームの導入が求められています。
本プロジェクトは、Spring、SpringMVC、MyBatis(通称 SSM)の組み合わせを基盤とした Web アプリケーションです。これにより、管理者、医師、看護師といった異なる役割を持つユーザーに対して、階層化されたアクセス制御と専用の操作インターフェースを提供します。データの一元管理により、コスト削減と業務処理の高速化を実現します。
技術スタック
開発環境および採用技術は以下の通り設定されています。
- バックエンド言語: Java (JDK 1.8 以上)
- フレームワーク: Spring Framework, SpringMVC, MyBatis
- フロントエンド: JSP, jQuery, Bootstrap
- データ永続化: MySQL 5.7+
- ビルドツール: Apache Maven
- Web サーバー: Apache Tomcat 9.x
- IDE: IntelliJ IDEA / Eclipse
機能モジュール詳細
システムはロールベースの権限制御を導入しており、各担当者には必要な最小権限のみが付与されます。
1. 管理者機能
システム全体の運用を担当するアカウントです。
- 職員管理: 医療スタッフ(医師・看護師)の登録、変更、削除を行います。
- 薬品在庫: 医薬品の価格更新や画像データのメンテナンスが可能です。
- 統計ダッシュボード: 入院患者の消費金額集計レポートを視覚的に確認できます。
- 設備管理: 病床の空き状況と割り当てを管理します。
2. 医師機能
治療計画および患者ケアの中核となります。
- 患者登録: 入院手続きを行い、疾患種別や担当医情報を記録します。
- 診療指示: 処方箋や検査オーダー(医嘱)の発行・変更を行います。
- カルテ更新: 患者の症状経過や家族連絡先を更新できます。
3. 看護師機能
日々の療養サポートと事務処理を担当します。
- 会計処理: 入院中の消耗品費や処置料などの収支入力を行います。
- 指示確認: 医師から出された治療命令を確認し、タスクを遂行します。
- 退院確認: 患者の入院期間や退院状況を照会します。
コア機能の実装サンプル
ここでは、セキュリティに関する重要な部分であるリクエスト認証とユーザーセッション管理のコード構造を示します。
アクセス制御フィルターの設計
CORS ポリシーの適用と、API トークンの検証を行うフィルタークラスです。すべてのリクエストに対して実行され、認証情報が正当か判定します。
package jp.med.core.security;
import com.med.service.AuthTokenService;
import com.med.utils.ResponseHandler;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Map;
/**
* リクエスト間の権限チェックを実装するインタセプター
*/
@Component
public class SessionValidationFilter extends HandlerInterceptorAdapter {
private static final String AUTHORIZATION_HEADER = "X-API-KEY";
@Autowired
private AuthTokenService tokenProvider;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Cross-Origin Resource Sharing のヘッダー設定
setCorsHeaders(response, request);
// 認証スキップが必要なエンドポイントのチェック
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
if (method.hasMethodAnnotation(AuthorizedIgnore.class)) {
return true;
}
} else {
return true;
}
// ヘッダーからのトークン抽出
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.isEmpty(bearerToken)) {
sendUnauthorizedResponse(response, "認証トークンが見つかりません");
return false;
}
// トークン有効性の確認とセッション属性への設定
Map<String, Object> userInfo = tokenProvider.validate(bearerToken);
if (userInfo == null) {
sendUnauthorizedResponse(response, "トークンが無効または期限切れです");
return false;
}
request.setAttribute("CURRENT_USER_ID", userInfo.get("uid"));
request.setAttribute("CURRENT_ROLE", userInfo.get("role"));
request.setAttribute("DB_TABLE_NAME", userInfo.get("source_table"));
return true;
}
private void setCorsHeaders(HttpServletResponse resp, HttpServletRequest req) {
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
resp.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-KEY");
resp.setHeader("Access-Control-Max-Age", "3600");
}
private void sendUnauthorizedResponse(HttpServletResponse resp, String msg) throws Exception {
resp.setContentType("application/json;charset=UTF-8");
PrintWriter writer = resp.getWriter();
writer.write(ResponseHandler.error(401, msg));
writer.flush();
writer.close();
}
}
認証コントローラーの実装
ユーザーのログイン、ログアウト、および基本情報取得を行うコントローラーです。RESTful なレスポンス形式を採用しています。
package jp.med.controller.api;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.med.entity.AccountInfo;
import com.med.service.AccountService;
import com.med.service.AuthTokenService;
import com.med.utils.MD5Encrypt;
import com.med.utils.PageResult;
import com.med.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* ユーザー認証およびセッション管理 API
*/
@RestController
@RequestMapping("/v1/accounts")
public class AccountSessionController {
@Autowired
private AccountService accountRepository;
@Autowired
private AuthTokenService tokenManager;
/**
* ログイン処理
*/
@PostMapping("/signin")
public Result executeLogin(@RequestBody Map<String, String> payload) {
String userId = payload.get("username");
String rawPass = payload.get("password");
EntityWrapper<AccountInfo> query = new EntityWrapper<>();
query.eq("username", userId);
AccountInfo foundUser = accountRepository.selectOne(query);
if (foundUser == null || !foundUser.getPassword().equals(rawPass)) {
return Result.fail("認証エラー: ID またはパスワードが一致しません");
}
// JWT トークン発行
String sessionToken = tokenManager.create(foundUser.getId(), foundUser.getUsername(), foundUser.getRole());
return Result.success(Map.of("access_token", sessionToken));
}
/**
* 新しいユーザー登録
*/
@PostMapping("/signup")
public Result registerNewUser(@RequestBody AccountInfo newAccount) {
EntityWrapper<AccountInfo> check = new EntityWrapper<>();
check.eq("username", newAccount.getUsername());
if (accountRepository.count(check) > 0) {
return Result.fail("重複エラー: このユーザー名は既に使用されています");
}
accountRepository.insert(newAccount);
return Result.success();
}
/**
* セッション終了(ログアウト)
*/
@GetMapping("/signout")
public Result terminateSession(HttpServletRequest currentRequest) {
currentRequest.getSession().invalidate();
return Result.success("正常にログアウトしました");
}
/**
* パスワード初期化
*/
@PostMapping("/reset-password")
public Result resetCredentials(@RequestParam String username) {
EntityWrapper<AccountInfo> target = new EntityWrapper<>();
target.eq("username", username);
AccountInfo user = accountRepository.selectOne(target);
if (user == null) {
return Result.fail("該当するアカウントは見つかりませんでした");
}
user.setPassword("default123"); // デフォルト値へ設定
accountRepository.updateById(user);
return Result.success("仮パスワードが発行されました: default123");
}
/**
* 条件付きユーザーリスト取得(ページング対応)
*/
@GetMapping("/list")
public Result searchUsers(@RequestParam Map<String, Object> filters) {
PageResult pageData = accountRepository.findPagedPage(filters);
return Result.success(pageData);
}
/**
* 特定ユーザーの情報を取得
*/
@GetMapping("/{id}")
public Result fetchProfile(@PathVariable String id) {
AccountInfo data = accountRepository.selectById(id);
return Result.success(data);
}
/**
* バッチ削除処理
*/
@DeleteMapping("/batch")
public Result removeBatch(@RequestBody List<String> ids) {
accountService.deleteBatchIds(ids);
return Result.success();
}
}