Javaコンソールアプリケーションにおける学生管理システムとユーザー認証の実装ガイド

システム要件とアーキテクチャ設計

本記事では、Javaを用いてコマンドラインベースの学生情報管理アプリケーションを構築します。開発は2つの段階に分け、まずコアとなるデータ操作機能を実装し、その後セキュアなアクセス制御層を追加します。

  • 基本階層: 学籍レコードの作成・検索・更新・削除(CRUD)を実装
  • 拡張階層: ユーザーアカウント管理、入力値の正規表現検証、セッション遷移制御

エンティティクラスの定義

データ構造を明確に分離するため、学生情報用クラスとユーザー認証用クラスを作成します。フィールドにはアクセサーとミューテーターを提供し、初期値設定メソッドを一元管理します。

// Student.java
public class StudentRecord {
    private String fullName;
    private int studentId;
    private Integer age;
    private String residence;

    public StudentRecord() {}

    public StudentRecord(String fullName, int studentId, Integer age, String residence) {
        this.fullName = fullName;
        this.studentId = studentId;
        this.age = age;
        this.residence = residence;
    }

    // アクセサー
    public String getFullName() { return fullName; }
    public void setFullName(String name) { this.fullName = name; }
    public int getStudentId() { return studentId; }
    public void setStudentId(int id) { this.studentId = id; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public String getResidence() { return residence; }
    public void setResidence(String loc) { this.residence = loc; }

    public void displayDetails() {
        System.out.printf("%-8d %-15s %-5d %-10s%n", studentId, fullName, age, residence);
    }
}
// UserAccount.java
public class UserAccount {
    private String username;
    private String password;
    private String identificationNumber;
    private String contactPhone;

    public UserAccount() {}

    public UserAccount(String username, String password, String idNumber, String phone) {
        this.username = username;
        this.password = password;
        this.identificationNumber = idNumber;
        this.contactPhone = phone;
    }

    public String getUsername() { return username; }
    public void setUsername(String user) { this.username = user; }
    public String getPassword() { return password; }
    public void setPassword(String pwd) { this.password = pwd; }
    public String getIdNumber() { return identificationNumber; }
    public void setIdNumber(String num) { this.identificationNumber = num; }
    public String getContactPhone() { return contactPhone; }
    public void setContactPhone(String phone) { this.contactPhone = phone; }
}

データ操作ロジックの実装

コレクションフレームワークを使用してメモリ内のデータストアを管理します。重複判定や検索処理はストリームAPIを活用し、可読性と保守性を向上させます。

import java.util.*;

public class StudentDataManager {
    private List<StudentRecord> records = new ArrayList<>();

    public StudentDataManager() {
        records.add(new StudentRecord("Tanaka", 1001, 20, "Tokyo"));
    }

    private Scanner console = new Scanner(System.in);
    private static final Map<String, Runnable> menuActions = Map.of(
        "1", this::registerNewStudent,
        "2", this::searchAllStudents,
        "3", this::updateStudentInfo,
        "4", this::removeStudentById,
        "5", null // Exit handled externally
    );

    public boolean executeMainLoop() {
        while (true) {
            printMenu();
            String selection = console.nextLine().trim();
            Runnable action = menuActions.get(selection);
            
            if (action == null && !selection.equals("5")) {
                System.out.println("無効な選択です。再度入力してください。");
                continue;
            }
            if (action != null) {
                action.run();
            } else {
                System.out.println("プログラムを終了しました。");
                return true;
            }
        }
    }

    private void printMenu() {
        System.out.println("=== 学生管理メニュー ===");
        System.out.println("1: 新規登録 | 2: 全件検索 | 3: 情報更新 | 4: 削除 | 5: 終了");
        System.out.print("オプションを選択: ");
    }

    public void registerNewStudent() {
        System.out.print("氏名: "); String name = console.nextLine();
        System.out.print("年齢: "); int age = console.nextInt(); console.nextLine();
        System.out.print("居住地: "); String loc = console.nextLine();
        System.out.print("学籍番号: "); int id = console.nextInt(); console.nextLine();

        long duplicateCheck = records.stream().filter(r -> r.getStudentId() == id).count();
        if (duplicateCheck > 0) {
            System.out.println("エラー: 指定された学籍番号は既に使用されています。");
        } else {
            records.add(new StudentRecord(name, id, age, loc));
            System.out.println("登録が完了しました。");
        }
    }

    public void searchAllStudents() {
        if (records.isEmpty()) {
            System.out.println("対象データが存在しません。");
            return;
        }
        System.out.printf("%n%-8s %-15s %-5s %-10s%n", "番号", "氏名", "年齢", "居住地");
        records.forEach(StudentRecord::displayDetails);
    }

    public void updateStudentInfo() {
        System.out.print("更新対象の学籍番号を入力: ");
        int targetId = console.nextInt(); console.nextLine();
        
        Optional<StudentRecord> target = records.stream()
            .filter(r -> r.getStudentId() == targetId)
            .findFirst();

        if (target.isPresent()) {
            System.out.println("現在の居住地: " + target.get().getResidence());
            System.out.print("新しい居住地: ");
            target.get().setResidence(console.nextLine());
            System.out.println("更新処理が正常に完了しました。");
        } else {
            System.out.println("該当する学籍番号が見つかりません。");
        }
    }

    public void removeStudentById() {
        System.out.print("削除対象の学籍番号: ");
        int removeId = console.nextInt(); console.nextLine();
        boolean removed = records.removeIf(r -> r.getStudentId() == removeId);
        System.out.println(removed ? "削除が成功しました。" : "指定されたIDのレコードは存在しません。");
    }
}

認証フローとバリデーションの実装

セキュリティ要件を満たすため、ユーザー情報の一意性チェック、フォーマット検証、および試行回数制限を導入します。正規表現パターンを使用し、入力サニタイジングを適用します。

import java.util.*;
import java.util.regex.*;

public class AuthenticationController {
    private List<UserAccount> accounts = new ArrayList<>();
    private Scanner scanner = new Scanner(System.in);

    private static final Pattern USER_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9]{2,14}$");
    private static final Pattern ID_CARD_PATTERN = Pattern.compile("^[1-9]\\d{16}[0-9Xx]$");
    private static final Pattern PHONE_PATTERN = Pattern.compile("^\\d{10,11}$");

    public AuthenticationController() {
        accounts.add(new UserAccount("admin_user", "pass123", "123456789012345678", "09012345678"));
    }

    public boolean runAuthSequence() {
        System.out.println("--- ユーザー認証画面 ---");
        System.out.println("1: ログイン | 2: 新規登録 | 3: パスワード再設定 | 4: 退出");
        String choice = scanner.nextLine().trim();

        switch (choice) {
            case "1": initiateLogin(); break;
            case "2": initiateRegistration(); break;
            case "3": initiatePasswordReset(); break;
            case "4": return true;
            default: System.out.println("無効なオプションです。");
        }
        return false;
    }

    private boolean validateFormat(String input, Pattern pattern) {
        return pattern.matcher(input).matches();
    }

    private void initiateRegistration() {
        System.out.print("ユーザー名: "); String user = scanner.nextLine();
        if (!USER_PATTERN.matcher(user).matches()) {
            System.out.println("ユーザー名の形式が不正です。(半角英数3〜15桁、数字のみ不可)");
            return;
        }

        System.out.print("パスワード: "); String pass1 = scanner.nextLine();
        System.out.print("確認用パスワード: "); String pass2 = scanner.nextLine();
        if (!pass1.equals(pass2)) {
            System.out.println("パスワードが一致しません。");
            return;
        }

        System.out.print("本人確認番号(18桁): "); String idNum = scanner.nextLine();
        if (!validateFormat(idNum, ID_CARD_PATTERN)) {
            System.out.println("本人確認番号の形式が不正です。");
            return;
        }

        System.out.print("携帯電話番号(10-11桁): "); String phone = scanner.nextLine();
        if (!validateFormat(phone, PHONE_PATTERN)) {
            System.out.println("携帯電話番号の形式が不正です。");
            return;
        }

        if (accounts.stream().anyMatch(a -> a.getUsername().equals(user))) {
            System.out.println("このユーザー名は既に登録されています。");
        } else {
            accounts.add(new UserAccount(user, pass1, idNum, phone));
            System.out.println("アカウントの作成が完了しました。");
        }
    }

    private void initiateLogin() {
        System.out.print("ユーザー名: "); String uname = scanner.nextLine();
        Optional<UserAccount> accountOpt = accounts.stream()
            .filter(a -> a.getUsername().equals(uname))
            .findFirst();

        if (accountOpt.isEmpty()) {
            System.out.println("エラー: 未登録のユーザーです。事前に登録が必要です。");
            return;
        }

        String captcha = generateCaptcha();
        System.out.printf("確認コード: [%s]%n", captcha);
        System.out.print("コードを入力: "); String userInput = scanner.nextLine();
        if (!userInput.equalsIgnoreCase(captcha)) {
            System.out.println("確認コードが正しくありません。");
            return;
        }

        int attempts = 0;
        while (attempts < 3) {
            System.out.print("パスワード: "); String inputPass = scanner.nextLine();
            if (accountOpt.get().getPassword().equals(inputPass)) {
                System.out.println("認証に成功しました。学生管理システムへ移行します。");
                new StudentDataManager().executeMainLoop();
                return;
            }
            attempts++;
            System.out.printf("失敗しました。残り %d 回の試行可能%n", 3 - attempts);
        }
        System.out.println("最大試行回数 exceeded。ログアウトします。");
    }

    private void initiatePasswordReset() {
        System.out.print("ユーザー名: "); String uname = scanner.nextLine();
        Optional<UserAccount> targetAccount = accounts.stream()
            .filter(a -> a.getUsername().equals(uname))
            .findFirst();

        if (targetAccount.isEmpty()) {
            System.out.println("エラー: 該当アカウントが見つかりません。");
            return;
        }

        System.out.print("本人確認番号: "); String checkId = scanner.nextLine();
        System.out.print("携帯電話番号: "); String checkPhone = scanner.nextLine();

        if (targetAccount.get().getIdNumber().equals(checkId) && 
            targetAccount.get().getContactPhone().equals(checkPhone)) {
            System.out.print("新しいパスワード: "); 
            targetAccount.get().setPassword(scanner.nextLine());
            System.out.println("パスワードの変更が正常に反映されました。");
        } else {
            System.out.println("エラー: 登録済みの個人情報と一致しません。変更できません。");
        }
    }

    private String generateCaptcha() {
        Random rand = new Random();
        StringBuilder builder = new StringBuilder();
        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        for (int i = 0; i < 4; i++) builder.append(chars.charAt(rand.nextInt(chars.length())));
        builder.insert(rand.nextInt(4), rand.nextInt(10));
        return builder.toString();
    }
}

実行フローの統合

上記のモジュールを組み合わせることで、入出力処理、ビジネスロジック、データ永続化(メモリ上)が分離された構成を実現します。コンソール経由で起動すれば、登録→認証→CRUD操作までの一連のワークフローをテスト可能です。

タグ: java-crud console-application user-authentication regex-validation stream-api

6月5日 18:16 投稿