Spring Boot、Vue.js、およびuni-appを活用したマンション管理システム

バックエンドフレームワーク: Spring Boot

Spring Bootは、Spring Frameworkを基盤としたアプリケーション開発を簡素化するための強力なツールです。内蔵のTomcat、Jetty、Undertowなどのウェブサーバーにより、追加の設定なしでアプリケーションを直接実行できます。Spring Bootの主要な利点の一つは、その「自動構成」機能です。プロジェクトの依存関係に応じてアプリケーションを自動で設定するため、各コンポーネントを手動で構成する手間が大幅に削減されます。また、Spring Data、Spring Security、Spring Cloudなど、豊富な既製機能やプラグインを提供しており、これらにより開発者は高品質なアプリケーションを迅速に構築し、他の技術との統合や拡張を容易に行えます。

フロントエンドフレームワーク: Vue.js

Vue.jsは、直感的で効率的なシングルページアプリケーション(SPA)の構築を可能にする、プログレッシブなJavaScriptフレームワークです。その中心にあるのは、仮想DOM(Virtual DOM)技術であり、これによってVue.jsはDOM操作を非常に効率的に行います。Vue.jsは、リアクティブなデータバインディング、仮想DOM、コンポーネントベースのアーキテクチャといったモダンな技術を採用しており、柔軟で高効率、そして保守が容易な開発モデルを提供します。データが変更されるとUIも自動的に更新されるため、開発者は手動でUIを更新するのではなく、データ処理そのものに集中できます。これにより、Vue.jsは簡潔で、柔軟かつ高効率な開発体験を実現します。

データ永続化フレームワーク: MyBatis-Plus

MyBatis-Plusは、MyBatisフレームワークを強化し、その開発プロセスを簡素化するために設計されたツールです。MySQL、Oracle、SQL Server、PostgreSQLなど、多様なデータベースをサポートするオープンソースのJavaフレームワークです。MyBatis-Plusは豊富なAPIとアノテーションを提供し、簡単な設定と利用でORM(Object-Relational Mapping)操作を実現し、手書きSQLの量を大幅に削減します。さらに、エンティティクラス、Mapperインターフェース、XMLマッピングファイルを自動生成するコードジェネレーターも提供されており、開発プロセスを劇的に効率化します。

また、MyBatis-Plusは、ページネーションクエリ、動的クエリ、楽観的ロック、パフォーマンス分析などの実用的な機能をサポートしており、開発者が効率的なデータ操作を行うのに役立ちます。MyBatis-Plusを使用することで、開発者は高品質なデータアクセス層のコードを迅速に開発し、生産性を向上させることができます。

システムテスト

システムテストは、開発されたアプリケーションが要件を満たし、安定して動作することを確認するために不可欠な工程です。本システムにおける主要なテスト目的は、多角的な視点から潜在的な問題を特定し、それを修正することで、システムの品質を保証することです。機能テストを通じて欠陥を発見し修正することで、システムに欠陥がないことを保証します。このプロセスにより、システムが顧客の要件を満たしていることを確認し、問題点や改善点を適時に修正します。テスト完了後、最終的な結論を導き出します。

テストの目的

マンション管理システムの開発サイクルにおいて、システムテストは欠かせない、忍耐を要するプロセスです。その重要性は、システムの品質と信頼性を保証する最後の砦であり、システム開発プロセス全体の最終チェックであるという点にあります。

システムテストの主な目的は、ユーザーが利用する際に問題が発生するのを防ぎ、ユーザーエクスペリエンスを向上させることです。ユーザーの使用を妨げないためにも、様々な角度やシナリオからシステムが遭遇する可能性のある問題を検討し、異なるシミュレーションを通じて欠陥を発見し解決する必要があります。テストプロセスを通じて、システムの品質状況、機能の健全性、論理の流れがスムーズであるかを確認できます。適切なシステムテストプロセスが完了すれば、システムの品質と使い勝手は大幅に向上します。テストの目標は、システムが要件定義書に合致しているかを検証し、要件定義書と矛盾する、または衝突する内容を発見することです。テストプロセス中は常にユーザーの視点に立って問題を検討し、非現実的なシナリオを避け、テスト時間の浪費を防ぎ、予期せぬ結果と実際の結果の不一致につながる可能性のある問題を回避する必要があります。

機能テスト

システム機能モジュールに対して、クリック操作、境界値入力、必須項目・任意項目の検証などの方法を用いた一連のブラックボックステストを実施します。テストケースを作成し、その内容に基づいてテストを行い、最終的なテスト結果を導き出します。

ログイン機能のテストシナリオ

本システムへのログイン時、アカウント名とパスワードなどを用いて検証を行います。ユーザーはデータベースに保存されているデータと一致する情報を入力する必要があります。いずれかの項目が間違っている場合、システムは入力エラーを通知します。このインターフェースでは、役割権限に対する適切な検証も行われます。例えば、管理者のアカウントで一般ユーザーとしてログインしようとするとエラーが発生します。ログイン機能のテストケースを以下の表に示します。

入力データ 期待結果 実際の結果 結果分析
ユーザー名:adminuser パスワード:securepass 認証コード:正確に入力 システムへログイン システムへのログインに成功 期待通り
ユーザー名:adminuser パスワード:wrongpass 認証コード:正確に入力 パスワードエラー パスワードが正しくありません、再入力してください 期待通り
ユーザー名:adminuser パスワード:securepass 認証コード:誤って入力 認証コードエラー 認証コード情報が間違っています 期待通り
ユーザー名:空 パスワード:securepass 認証コード:正確に入力 ユーザー名必須 ユーザー名を入力してください 期待通り
ユーザー名:adminuser パスワード:空 認証コード:正確に入力 パスワードエラー パスワードが正しくありません、再入力してください 期待通り
ユーザー管理機能のテストシナリオ

ユーザー管理機能には、ユーザーの追加、編集、削除、検索が含まれます。ユーザー追加時、必須項目が未入力の場合にシステムが非空検証を行うかを確認します。既存のユーザー情報を追加した場合、ユーザー名が既に利用されていることを警告するかを検証します。ユーザー情報削除時には、システムが操作の確認を行うかを確認します。ユーザー情報変更後、ページに変更が反映されるかを検証します。ユーザー管理のテストケースを以下の表に示します。

入力データ 期待結果 実際の結果 結果分析
ユーザー基本情報を入力 追加成功、ユーザーリストに表示 ユーザーがリストに表示された 期待通り
ユーザー情報を変更 編集成功、変更された情報が反映 ユーザー情報が変更された 期待通り
ユーザーを選択して削除 システムがユーザー削除の確認を求め、確認後にユーザーが削除される システムがユーザー削除の確認を求め、確認後にユーザー情報が見つからない 期待通り
ユーザー追加時にユーザー名を未入力 ユーザー名は空にできませんと表示 ユーザー名は空にできませんと表示 期待通り
既存のユーザー名を入力 追加失敗、ユーザー名重複を警告 追加失敗、ユーザー名重複を警告 期待通り

テスト結果のまとめ

本システムでは主にブラックボックステストを実施し、ユーザーが各機能を操作するシナリオをシミュレートしてテストケースを作成し、テストを行いました。これにより、システムフローの正確性を保証します。システムテストは、システムをより完璧にし、利用可能性を高めるために不可欠です。

このシステムのテストは、システムの機能モジュールが当初の設計思想を満たしているか、各機能モジュールのロジックが正しいかを検証することを主な目的としました。このシステムは、利用者が操作しやすいように、過度に複雑なロジック処理を必要としません。テストの最終目的も、ユーザーの利用を中心に展開されました。テストプロセス中のすべてのシナリオはユーザー要件に合致しているべきであり、要件目標から逸脱してはなりません。問題に遭遇した場合は、ユーザーの視点に立って考える必要があります。一連のテストプロセスを経て得られた最終的なテスト結果から、実現されたシステムが機能面および性能面で設計要件を満たしていることが確認されました。

コード例

ここでは、ログイン処理と認証インターセプターの一部実装を示します。


import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper; // JSONシリアライズ用
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.UUID;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;


// API応答のためのユーティリティクラス(成功/エラーなど)
class ApiResponse {
    private int code;
    private String message;
    private Object data;

    public ApiResponse(int code, String message, Object data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static ApiResponse success(String message, Object data) {
        return new ApiResponse(200, message, data);
    }

    public static ApiResponse error(int code, String message) {
        return new ApiResponse(code, message, null);
    }

    // Getters
    public int getCode() { return code; }
    public String getMessage() { return message; }
    public Object getData() { return data; }
}

// ユーザーエンティティ例 (デモンストレーション用のシンプルな構造を想定)
class SystemUser {
    private Long id;
    private String loginId;
    private String passwordHash; // ハッシュ化されたパスワードを格納
    private String userRole;

    public SystemUser(Long id, String loginId, String passwordHash, String userRole) {
        this.id = id;
        this.loginId = loginId;
        this.passwordHash = passwordHash;
        this.userRole = userRole;
    }

    // Getters
    public Long getId() { return id; }
    public String getLoginId() { return loginId; }
    public String getPasswordHash() { return passwordHash; }
    public String getUserRole() { return userRole; }
}

// トークンエンティティ例
class AuthToken {
    private Long id;
    private Long userId;
    private String userName;
    private String entityTable; // 例: "users", "administrators"
    private String assignedRole;
    private String tokenValue;
    private LocalDateTime createdTime;
    private LocalDateTime expiryTime;

    public AuthToken(Long userId, String userName, String entityTable, String assignedRole, String tokenValue, LocalDateTime expiryTime) {
        this.userId = userId;
        this.userName = userName;
        this.entityTable = entityTable;
        this.assignedRole = assignedRole;
        this.tokenValue = tokenValue;
        this.createdTime = LocalDateTime.now();
        this.expiryTime = expiryTime;
    }

    // Getters and Setters (簡略化のため一部のみ)
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Long getUserId() { return userId; }
    public String getTokenValue() { return tokenValue; }
    public void setTokenValue(String tokenValue) { this.tokenValue = tokenValue; }
    public LocalDateTime getExpiryTime() { return expiryTime; }
    public void setExpiryTime(LocalDateTime expiryTime) { this.expiryTime = expiryTime; }
    public String getAssignedRole() { return assignedRole; }
    public String getUserName() { return userName; }
    public String getEntityTable() { return entityTable; }
}

// ユーザー認証を管理するサービスインターフェース
interface UserAuthService {
    SystemUser findUserByLoginId(String loginId);
    boolean verifyPassword(String rawPassword, String storedHashedPassword);
}

// トークンを管理するサービスインターフェース
interface TokenManagementService {
    AuthToken issueNewToken(Long userId, String userName, String entityTable, String userRole);
    AuthToken findTokenByValue(String tokenValue);
    void updateToken(AuthToken token); // 現実ではJPAなどの永続化処理
    void saveToken(AuthToken token); // 現実ではJPAなどの永続化処理
}

// UserAuthServiceのモック実装 (実際のアプリケーションではDBをクエリします)
class MockUserAuthService implements UserAuthService {
    private static final SystemUser ADMIN_USER = new SystemUser(1L, "admin", "hashed_securepass", "管理者");
    @Override
    public SystemUser findUserByLoginId(String loginId) {
        return "admin".equals(loginId) ? ADMIN_USER : null;
    }

    @Override
    public boolean verifyPassword(String rawPassword, String storedHashedPassword) {
        // 実際のアプリケーションでは、パスワードのハッシュ化と検証にBCryptなどを使用します
        return "securepass".equals(rawPassword) && "hashed_securepass".equals(storedHashedPassword);
    }
}

// TokenManagementServiceのモック実装 (実際のアプリケーションではDBと連携します)
class MockTokenManagementService implements TokenManagementService {
    private final Map<String, AuthToken> tokenStore = new ConcurrentHashMap<>();
    private Long nextId = 1L;

    @Override
    public AuthToken issueNewToken(Long userId, String userName, String entityTable, String userRole) {
        // 40文字のランダムなトークン値を生成
        String newTokenValue = UUID.randomUUID().toString().replace("-", "") + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
        LocalDateTime expiry = LocalDateTime.now().plusHours(1); // 1時間後に期限切れ

        AuthToken existingToken = tokenStore.values().stream()
                                            .filter(t -> t.getUserId().equals(userId) && t.getAssignedRole().equals(userRole))
                                            .findFirst().orElse(null);

        if (existingToken != null) {
            existingToken.setTokenValue(newTokenValue);
            existingToken.setExpiryTime(expiry);
            return existingToken;
        } else {
            AuthToken token = new AuthToken(userId, userName, entityTable, userRole, newTokenValue, expiry);
            token.setId(nextId++);
            tokenStore.put(newTokenValue, token);
            return token;
        }
    }

    @Override
    public AuthToken findTokenByValue(String tokenValue) {
        return tokenStore.get(tokenValue);
    }

    @Override
    public void updateToken(AuthToken token) {
        // このモックでは、既存のトークンが更新された場合は既にマップに反映されています。
        // 新規トークンの場合は saveToken が呼び出されます。
    }

    @Override
    public void saveToken(AuthToken token) {
        tokenStore.put(token.getTokenValue(), token);
    }
}


// 認証をバイパスするためのアノテーション
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface PublicAccess { }

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private UserAuthService userAuthService = new MockUserAuthService(); // 実際には適切なサービスをインジェクト
    @Autowired
    private TokenManagementService tokenManagementService = new MockTokenManagementService(); // 実際には適切なサービスをインジェクト

    /**
     * ユーザーログインエンドポイント
     */
    @PublicAccess
    @PostMapping(value = "/login")
    public ApiResponse handleLogin(@RequestParam String loginId,
                                   @RequestParam String passcode,
                                   @RequestParam(required = false) String verificationCode) {
        SystemUser user = userAuthService.findUserByLoginId(loginId);
        if (user == null || !userAuthService.verifyPassword(passcode, user.getPasswordHash())) {
            return ApiResponse.error(400, "ユーザー名またはパスワードが正しくありません");
        }
        // 実際のシナリオでは、提供されたキャプチャもここで検証する場合があります。

        AuthToken authToken = tokenManagementService.issueNewToken(user.getId(), user.getLoginId(), "system_users", user.getUserRole());
        return ApiResponse.success("ログイン成功", Collections.singletonMap("accessToken", authToken.getTokenValue()));
    }
}

@Component
public class SecurityInterceptor implements HandlerInterceptor {

    public static final String AUTH_HEADER_NAME = "X-Auth-Token"; // 認証ヘッダー名

    @Autowired
    private TokenManagementService tokenManagementService = new MockTokenManagementService(); // 実際には適切なサービスをインジェクト

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // CORSヘッダー設定(実際のプロダクションではSpring SecurityのCORS設定を使うべきです)
        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", "X-Requested-With, Content-Type, Authorization, " + AUTH_HEADER_NAME); // 更新されたヘッダー
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Credentials", "true");

        // OPTIONSリクエストは前処理なしで通過
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpStatus.OK.value());
            return false;
        }

        // PublicAccessアノテーションが付与されたメソッドは認証をスキップ
        PublicAccess publicAccessAnnotation = null;
        if (handler instanceof HandlerMethod) {
            publicAccessAnnotation = ((HandlerMethod) handler).getMethodAnnotation(PublicAccess.class);
        }

        if (publicAccessAnnotation != null) {
            return true;
        }

        // リクエストヘッダーからトークンを取得
        String token = request.getHeader(AUTH_HEADER_NAME);

        AuthToken validatedToken = null;
        if (StringUtils.hasText(token)) {
            validatedToken = tokenManagementService.findTokenByValue(token);
        }

        // トークンが存在し、有効期限内であれば認証成功
        if (validatedToken != null && validatedToken.getExpiryTime().isAfter(LocalDateTime.now())) {
            // ユーザー情報をセッションに格納 (またはよりセキュアなコンテキストに)
            request.getSession().setAttribute("currentUserId", validatedToken.getUserId());
            request.getSession().setAttribute("currentUserRole", validatedToken.getAssignedRole());
            request.getSession().setAttribute("currentUserTable", validatedToken.getEntityTable());
            request.getSession().setAttribute("currentUserName", validatedToken.getUserName());
            return true;
        }

        // 認証失敗
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter writer = response.getWriter();
        try {
            // ObjectMapperでJSONシリアライズ
            ObjectMapper mapper = new ObjectMapper();
            writer.print(mapper.writeValueAsString(ApiResponse.error(HttpStatus.UNAUTHORIZED.value(), "認証が必要です。再度ログインしてください。")));
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        return false;
    }
}

データベーススキーマ例

ここでは、認証トークンを管理するためのテーブルスキーマを示します。


-- 認証トークン情報を格納するテーブル
DROP TABLE IF EXISTS `auth_tokens`;
CREATE TABLE `auth_tokens` (
  `token_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主キー',
  `user_id` BIGINT(20) NOT NULL COMMENT '関連するユーザーID',
  `user_name` VARCHAR(100) NOT NULL COMMENT 'ユーザー名',
  `associated_table` VARCHAR(100) DEFAULT NULL COMMENT '関連エンティティテーブル名(例: system_users, residents)',
  `user_role` VARCHAR(100) DEFAULT NULL COMMENT 'ユーザーの役割',
  `token_value` VARCHAR(200) NOT NULL COMMENT '認証トークン文字列',
  `creation_timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'トークン作成日時',
  `expiration_timestamp` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'トークンの有効期限日時',
  PRIMARY KEY (`token_id`) USING BTREE,
  UNIQUE KEY `idx_token_value` (`token_value`) -- トークン値の一意性を保証
) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='認証トークン管理テーブル';

-- サンプルデータ
INSERT INTO `auth_tokens` (`token_id`, `user_id`, `user_name`, `associated_table`, `user_role`, `token_value`, `creation_timestamp`, `expiration_timestamp`) VALUES
(101, 1, 'admin_user', 'system_users', '管理者', 'abcdef1234567890abcdef1234567890abcdef', '2023-01-01 10:00:00', '2023-01-01 11:00:00'),
(102, 5, 'john_doe', 'residents', '居住者', 'fedcba0987654321fedcba0987654321fedcba', '2023-01-01 10:05:00', '2023-01-01 11:05:00'),
(103, 10, 'jane_smith', 'residents', '居住者', 'zyxwvu1234567890zyxwvu1234567890zyxwvu', '2023-01-01 10:10:00', '2023-01-01 11:10:00');

タグ: SpringBoot vue.js uni-app MyBatis-Plus authentication

7月22日 16:03 投稿