|
|
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<String, UserAccount> accounts = new ConcurrentHashMap<>();
|
|
|
|
|
|
/**
|
|
|
* 创建账号仓库并立即加载磁盘中的账号数据.
|
|
|
*/
|
|
|
public AccountRepository() {
|
|
|
loadAccounts();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 根据用户名与密码查找匹配的账号.
|
|
|
*
|
|
|
* @param username 用户名.
|
|
|
* @param password 密码.
|
|
|
* @return 匹配成功时返回账号信息,否则返回空结果.
|
|
|
*/
|
|
|
public Optional<UserAccount> 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<UserAccount> 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();
|
|
|
}
|
|
|
}
|