You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
math-learing/src/main/java/com/personalproject/auth/AccountRepository.java

325 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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();
}
}