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.
105 lines
5.0 KiB
105 lines
5.0 KiB
package com.mydreamjob.auth;
|
|
|
|
import org.springframework.dao.DataAccessException;
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
|
import org.springframework.stereotype.Component;
|
|
import org.springframework.transaction.support.TransactionTemplate;
|
|
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
|
|
/**
|
|
* S03 定向整改:管理员账号创建的**独立事务组件**。
|
|
*
|
|
* <p>为什么不用 JPA/Hibernate:并发初始化时数据库唯一约束会成为正常并发控制手段,
|
|
* 而 Hibernate 的 {@code SqlExceptionHelper} 会在业务代码捕获异常**之前**以 ERROR 级别
|
|
* 记录整条语句与绑定值 —— 管理员手机号因此进入生产可见日志。</p>
|
|
*
|
|
* <p>本组件改用 {@link JdbcTemplate} 的参数化语句:</p>
|
|
* <ul>
|
|
* <li>冲突在 JDBC 层被捕获,**不经过** Hibernate 日志体系;</li>
|
|
* <li>原始数据库异常与 message/cause **一律丢弃**,对外只返回结果码;</li>
|
|
* <li>{@code users} 与 {@code user_roles} 在**同一事务**内提交 ——
|
|
* 角色写入失败时用户创建整体回滚;</li>
|
|
* <li>不打印 SQL、手机号或绑定参数。</li>
|
|
* </ul>
|
|
*/
|
|
@Component
|
|
public class AdminBootstrapCreator {
|
|
|
|
/** 创建结果:成功创建 / 数据库唯一约束冲突。 */
|
|
public enum CreateResult {
|
|
CREATED,
|
|
CONFLICT
|
|
}
|
|
|
|
/** 引导账号的最小状态视图(不含任何敏感字段输出)。 */
|
|
public record BootstrapUser(Long id, String role, boolean active, boolean verified,
|
|
boolean passwordAbsent, List<String> roles) {
|
|
}
|
|
|
|
private final JdbcTemplate jdbcTemplate;
|
|
private final TransactionTemplate transactionTemplate;
|
|
|
|
/**
|
|
* 使用**独立 REQUIRES_NEW 事务**:冲突必须只回滚本次创建,不得把调用方的外层事务
|
|
* 标记为 rollback-only(否则调用方提交时会抛 {@code UnexpectedRollbackException})。
|
|
*/
|
|
public AdminBootstrapCreator(JdbcTemplate jdbcTemplate,
|
|
org.springframework.transaction.PlatformTransactionManager transactionManager) {
|
|
this.jdbcTemplate = jdbcTemplate;
|
|
this.transactionTemplate = new TransactionTemplate(transactionManager);
|
|
this.transactionTemplate.setPropagationBehavior(
|
|
org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
|
}
|
|
|
|
/**
|
|
* 在同一事务内写入 {@code users} 与 {@code user_roles}。
|
|
*
|
|
* @return {@link CreateResult#CREATED} 或 {@link CreateResult#CONFLICT};
|
|
* 冲突时不抛出任何携带原始数据库信息的异常
|
|
*/
|
|
public CreateResult createAdmin(String phone, String nickname) {
|
|
try {
|
|
transactionTemplate.executeWithoutResult(status -> {
|
|
jdbcTemplate.update(
|
|
"INSERT INTO users (phone, nickname, password_hash, role, is_active, is_verified, "
|
|
+ "created_at, updated_at) VALUES (?, ?, NULL, 'ADMIN', 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
|
phone, nickname);
|
|
Long userId = jdbcTemplate.queryForObject(
|
|
"SELECT id FROM users WHERE phone = ?", Long.class, phone);
|
|
jdbcTemplate.update("INSERT INTO user_roles (user_id, role) VALUES (?, 'ADMIN')", userId);
|
|
});
|
|
return CreateResult.CREATED;
|
|
} catch (DataAccessException e) {
|
|
// 唯一约束冲突(并发初始化)、角色写入失败或任何 JDBC 层失败:事务已整体回滚。
|
|
// 刻意**丢弃**原始异常:它的 message 可能包含 SQL 与绑定值(例如手机号)。
|
|
// 调用方据此重新查询,决定是「幂等成功」还是抛出不带 cause 的固定安全异常。
|
|
return CreateResult.CONFLICT;
|
|
}
|
|
}
|
|
|
|
/** 按手机号读取引导所需的最小状态;只读、参数化,不经过 Hibernate。 */
|
|
public Optional<BootstrapUser> findByPhone(String phone) {
|
|
List<BootstrapUser> rows = jdbcTemplate.query(
|
|
"SELECT id, role, is_active, is_verified, password_hash FROM users WHERE phone = ?",
|
|
(rs, i) -> new BootstrapUser(
|
|
rs.getLong("id"),
|
|
rs.getString("role"),
|
|
rs.getInt("is_active") == 1,
|
|
rs.getInt("is_verified") == 1,
|
|
rs.getString("password_hash") == null,
|
|
List.of()),
|
|
phone);
|
|
if (rows.isEmpty()) {
|
|
return Optional.empty();
|
|
}
|
|
BootstrapUser base = rows.get(0);
|
|
List<String> roles = jdbcTemplate.queryForList(
|
|
"SELECT role FROM user_roles WHERE user_id = ?", String.class, base.id());
|
|
return Optional.of(new BootstrapUser(base.id(), base.role(), base.active(),
|
|
base.verified(), base.passwordAbsent(), roles));
|
|
}
|
|
|
|
/** 是否存在活动 ADMIN(不涉及手机号,可用 JPA 仓储)。 */
|
|
}
|
|
|