package com.personalproject.auth; import com.personalproject.model.DifficultyLevel; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; import java.util.Map; import java.util.Optional; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; /** * 存放用户账号并提供认证和注册查询能力. */ public final class AccountRepository { private static final Path ACCOUNTS_DIRECTORY = Paths.get("user_data", "accounts"); private static final String FILE_EXTENSION = ".properties"; private final Map accounts = new ConcurrentHashMap<>(); /** * 创建账号仓库并立即加载磁盘中的账号数据. */ public AccountRepository() { loadAccounts(); } /** * 根据用户名与密码查找匹配的账号. * * @param username 用户名. * @param password 密码. * @return 匹配成功时返回账号信息,否则返回空结果. */ public Optional authenticate(String username, String password) { if (username == null || password == null) { return Optional.empty(); } String normalizedUsername = username.trim(); String normalizedPassword = password.trim(); UserAccount account = accounts.get(normalizedUsername); if (account == null || !account.isRegistered()) { return Optional.empty(); } if (!account.password().equals(hashPassword(normalizedPassword))) { return Optional.empty(); } return Optional.of(account); } /** * 使用电子邮箱注册新用户账号. * * @param username 用户名 * @param email 邮箱地址 * @param difficultyLevel 选择的难度级别 * @return 若注册成功则返回 true,若用户名已存在则返回 false */ public synchronized boolean registerUser(String username, String email, DifficultyLevel difficultyLevel) { if (username == null || email == null || difficultyLevel == null) { return false; } String normalizedUsername = username.trim(); String normalizedEmail = email.trim(); if (normalizedUsername.isEmpty() || normalizedEmail.isEmpty()) { return false; } UserAccount existing = accounts.get(normalizedUsername); if (existing != null && existing.isRegistered()) { return false; } if (isEmailInUse(normalizedEmail, normalizedUsername)) { return false; } LocalDateTime registrationDate = existing != null ? existing.registrationDate() : LocalDateTime.now(); UserAccount account = new UserAccount( normalizedUsername, normalizedEmail, "", difficultyLevel, registrationDate, false); accounts.put(normalizedUsername, account); persistAccount(account); return true; } /** * 在注册后为用户设置密码. * * @param username 用户名 * @param password 要设置的密码 * @return 设置成功返回 true,若用户不存在则返回 false */ public synchronized boolean setPassword(String username, String password) { if (username == null || password == null) { return false; } String normalizedUsername = username.trim(); UserAccount account = accounts.get(normalizedUsername); if (account == null || account.isRegistered()) { return false; } UserAccount updatedAccount = new UserAccount( account.username(), account.email(), hashPassword(password.trim()), account.difficultyLevel(), account.registrationDate(), true); accounts.put(normalizedUsername, updatedAccount); persistAccount(updatedAccount); return true; } /** * 修改现有用户的密码. * * @param username 用户名 * @param oldPassword 当前密码 * @param newPassword 新密码 * @return 修改成功返回 true,若旧密码错误或用户不存在则返回 false */ public synchronized boolean changePassword(String username, String oldPassword, String newPassword) { if (username == null || oldPassword == null || newPassword == null) { return false; } String normalizedUsername = username.trim(); UserAccount account = accounts.get(normalizedUsername); if (account == null || !account.isRegistered()) { return false; } if (!account.password().equals(hashPassword(oldPassword.trim()))) { return false; } UserAccount updatedAccount = new UserAccount( account.username(), account.email(), hashPassword(newPassword.trim()), account.difficultyLevel(), account.registrationDate(), true); accounts.put(normalizedUsername, updatedAccount); persistAccount(updatedAccount); return true; } /** * 移除未完成注册的用户,便于重新注册. * * @param username 待移除的用户名 */ public synchronized void removeUnverifiedUser(String username) { if (username == null) { return; } String normalizedUsername = username.trim(); UserAccount account = accounts.get(normalizedUsername); if (account != null && !account.isRegistered()) { accounts.remove(normalizedUsername); deleteAccountFile(normalizedUsername); } } /** * 检查用户是否存在. * * @param username 待检查的用户名 * @return 若存在则返回 true,否则返回 false */ public boolean userExists(String username) { if (username == null) { return false; } return accounts.containsKey(username.trim()); } /** * 按用户名获取用户账户. * * @param username 用户名 * @return 若找到则返回包含用户账户的 Optional */ public Optional getUser(String username) { if (username == null) { return Optional.empty(); } return Optional.ofNullable(accounts.get(username.trim())); } private void loadAccounts() { try { if (!Files.exists(ACCOUNTS_DIRECTORY)) { Files.createDirectories(ACCOUNTS_DIRECTORY); return; } try (var paths = Files.list(ACCOUNTS_DIRECTORY)) { paths .filter(path -> path.getFileName().toString().endsWith(FILE_EXTENSION)) .forEach(this::loadAccountFromFile); } } catch (IOException exception) { throw new IllegalStateException("加载账户数据失败", exception); } } private void loadAccountFromFile(Path file) { Properties properties = new Properties(); try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { properties.load(reader); } catch (IOException exception) { System.err.println("读取账户文件失败: " + file + ",原因: " + exception.getMessage()); return; } String username = properties.getProperty("username"); if (username == null || username.isBlank()) { return; } String email = properties.getProperty("email", ""); String passwordHash = properties.getProperty("passwordHash", ""); String difficultyValue = properties.getProperty("difficulty", DifficultyLevel.PRIMARY.name()); String registrationDateValue = properties.getProperty("registrationDate"); String registeredValue = properties.getProperty("registered", "false"); try { DifficultyLevel difficultyLevel = DifficultyLevel.valueOf(difficultyValue); LocalDateTime registrationDate = registrationDateValue == null ? LocalDateTime.now() : LocalDateTime.parse(registrationDateValue); boolean registered = Boolean.parseBoolean(registeredValue); UserAccount account = new UserAccount( username, email, passwordHash, difficultyLevel, registrationDate, registered); accounts.put(username, account); } catch (Exception exception) { System.err.println("解析账户文件失败: " + file + ",原因: " + exception.getMessage()); } } private void persistAccount(UserAccount account) { Properties properties = new Properties(); properties.setProperty("username", account.username()); properties.setProperty("email", account.email()); properties.setProperty("passwordHash", account.password()); properties.setProperty("difficulty", account.difficultyLevel().name()); properties.setProperty("registrationDate", account.registrationDate().toString()); properties.setProperty("registered", Boolean.toString(account.isRegistered())); Path targetFile = accountFile(account.username()); try { if (!Files.exists(ACCOUNTS_DIRECTORY)) { Files.createDirectories(ACCOUNTS_DIRECTORY); } try (Writer writer = Files.newBufferedWriter(targetFile, StandardCharsets.UTF_8)) { properties.store(writer, "User account data"); } } catch (IOException exception) { throw new IllegalStateException("保存账户数据失败: " + account.username(), exception); } } private void deleteAccountFile(String username) { Path file = accountFile(username); try { Files.deleteIfExists(file); } catch (IOException exception) { System.err.println("删除账户文件失败: " + file + ",原因: " + exception.getMessage()); } } private boolean isEmailInUse(String email, String currentUsername) { return accounts.values().stream() .anyMatch(account -> account.email().equalsIgnoreCase(email) && !account.username().equals(currentUsername)); } private Path accountFile(String username) { return ACCOUNTS_DIRECTORY.resolve(username + FILE_EXTENSION); } private String hashPassword(String password) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hashBytes = digest.digest(password.getBytes(StandardCharsets.UTF_8)); return toHex(hashBytes); } catch (NoSuchAlgorithmException exception) { throw new IllegalStateException("当前运行环境不支持SHA-256算法", exception); } } private String toHex(byte[] bytes) { StringBuilder builder = new StringBuilder(bytes.length * 2); for (byte value : bytes) { builder.append(String.format("%02x", value)); } return builder.toString(); } }