package com.talentcard.server.business.admin.auth;
|
|
import java.nio.charset.StandardCharsets;
|
import java.security.MessageDigest;
|
import java.security.SecureRandom;
|
import java.sql.Timestamp;
|
import java.time.LocalDateTime;
|
import java.util.Base64;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
import com.talentcard.server.business.DatabaseRows;
|
import com.talentcard.server.business.merchant.auth.MerchantPasswordHasher;
|
import com.talentcard.server.common.BusinessException;
|
import com.talentcard.server.common.ErrorCode;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
@Service
|
public class AdminAuthService {
|
private static final int MAX_FAILURES = 5;
|
private static final int ACCESS_TOKEN_SECONDS = 8 * 60 * 60;
|
private static final SecureRandom RANDOM = new SecureRandom();
|
|
private final JdbcTemplate jdbc;
|
|
public AdminAuthService(JdbcTemplate jdbc) {
|
this.jdbc = jdbc;
|
}
|
|
@Transactional
|
public Map<String, Object> login(AdminLoginRequest request, String requestId) {
|
String account = request.account().trim();
|
List<Map<String, Object>> rows = rows("""
|
select id operator_id, password_hash, display_name, role_code, status,
|
login_failed_count, locked_until
|
from admin_operator
|
where account = ? and deleted = 0
|
limit 1
|
""", account);
|
if (rows.isEmpty()) {
|
audit(null, account, false, "INVALID_CREDENTIALS", requestId);
|
throw invalidCredentials();
|
}
|
|
Map<String, Object> operator = rows.get(0);
|
Long operatorId = number(operator.get("operatorId"));
|
if (number(operator.get("status")) != 1) {
|
audit(operatorId, account, false, "ACCOUNT_DISABLED", requestId);
|
throw new BusinessException(ErrorCode.FORBIDDEN, "当前管理员账号已停用");
|
}
|
LocalDateTime lockedUntil = time(operator.get("lockedUntil"));
|
if (lockedUntil != null && lockedUntil.isAfter(LocalDateTime.now())) {
|
audit(operatorId, account, false, "ACCOUNT_LOCKED", requestId);
|
throw new BusinessException(ErrorCode.FORBIDDEN, "登录尝试过多,请15分钟后再试");
|
}
|
if (!MerchantPasswordHasher.matches(request.password(), String.valueOf(operator.get("passwordHash")))) {
|
int failures = Math.toIntExact(number(operator.get("loginFailedCount")) + 1);
|
LocalDateTime nextLockedUntil = failures >= MAX_FAILURES ? LocalDateTime.now().plusMinutes(15) : null;
|
jdbc.update("update admin_operator set login_failed_count = ?, locked_until = ? where id = ?",
|
failures, nextLockedUntil, operatorId);
|
audit(operatorId, account, false, "INVALID_CREDENTIALS", requestId);
|
throw invalidCredentials();
|
}
|
|
jdbc.update("update admin_operator set login_failed_count = 0, locked_until = null, last_login_at = now() where id = ?", operatorId);
|
audit(operatorId, account, true, null, requestId);
|
return issueSession(operator);
|
}
|
|
@Transactional
|
public Map<String, Object> logout(String authorization) {
|
String token = bearer(authorization);
|
if (!token.isBlank()) {
|
jdbc.update("update admin_session set revoked_at = now() where access_token_hash = ? and revoked_at is null", sha256(token));
|
}
|
return Map.of("loggedOut", true);
|
}
|
|
@Transactional
|
public Long authenticate(String authorization) {
|
String token = bearer(authorization);
|
if (token.isBlank()) throw new BusinessException(ErrorCode.UNAUTHORIZED, "请先登录管理后台");
|
List<Map<String, Object>> rows = rows("""
|
select s.id session_id, s.operator_id, o.status
|
from admin_session s join admin_operator o on o.id = s.operator_id
|
where s.access_token_hash = ? and s.revoked_at is null and s.access_expires_at > now()
|
and o.deleted = 0
|
limit 1
|
""", sha256(token));
|
if (rows.isEmpty()) throw new BusinessException(ErrorCode.UNAUTHORIZED, "登录已失效,请重新登录");
|
if (number(rows.get(0).get("status")) != 1) {
|
throw new BusinessException(ErrorCode.FORBIDDEN, "当前管理员账号已停用");
|
}
|
jdbc.update("update admin_session set last_used_at = now() where id = ?", number(rows.get(0).get("sessionId")));
|
return number(rows.get(0).get("operatorId"));
|
}
|
|
private Map<String, Object> issueSession(Map<String, Object> operator) {
|
String accessToken = token();
|
LocalDateTime now = LocalDateTime.now();
|
jdbc.update("insert into admin_session(operator_id, access_token_hash, access_expires_at) values (?, ?, ?)",
|
number(operator.get("operatorId")), sha256(accessToken), now.plusSeconds(ACCESS_TOKEN_SECONDS));
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("accessToken", accessToken);
|
result.put("expiresIn", ACCESS_TOKEN_SECONDS);
|
result.put("operator", Map.of(
|
"id", number(operator.get("operatorId")),
|
"name", String.valueOf(operator.get("displayName")),
|
"roleCode", String.valueOf(operator.get("roleCode"))));
|
return result;
|
}
|
|
private void audit(Long operatorId, String account, boolean success, String reason, String requestId) {
|
jdbc.update("insert into admin_login_audit(operator_id, account, success, failure_reason, request_id) values (?, ?, ?, ?, ?)",
|
operatorId, account, success ? 1 : 0, reason, requestId);
|
}
|
|
private BusinessException invalidCredentials() {
|
return new BusinessException(ErrorCode.UNAUTHORIZED, "账号或密码错误");
|
}
|
|
private List<Map<String, Object>> rows(String sql, Object... args) {
|
return DatabaseRows.normalize(jdbc.queryForList(sql, args));
|
}
|
|
private Long number(Object value) {
|
return value instanceof Number number ? number.longValue() : 0L;
|
}
|
|
private LocalDateTime time(Object value) {
|
if (value instanceof LocalDateTime dateTime) return dateTime;
|
if (value instanceof Timestamp timestamp) return timestamp.toLocalDateTime();
|
return null;
|
}
|
|
private String token() {
|
byte[] bytes = new byte[32];
|
RANDOM.nextBytes(bytes);
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
}
|
|
private String bearer(String authorization) {
|
return authorization == null ? "" : authorization.replaceFirst("(?i)^Bearer\\s+", "").trim();
|
}
|
|
private String sha256(String value) {
|
try {
|
byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
|
StringBuilder result = new StringBuilder();
|
for (byte item : digest) result.append(String.format("%02x", item));
|
return result.toString();
|
} catch (Exception exception) {
|
throw new IllegalStateException("Unable to process admin session", exception);
|
}
|
}
|
}
|