wenminjie
2026-08-03 ddc84625dcdb22a1ec2407f6adc7eb2e7a839ef0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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);
        }
    }
}