fix:登录流程改造

This commit is contained in:
waner
2026-05-26 11:29:44 +08:00
parent fe957fe599
commit 0f1c24caf9
22 changed files with 413 additions and 903 deletions
+9 -7
View File
@@ -1,8 +1,10 @@
# TMS 初始化 SQL 执行说明
当前推荐使用 [V1__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V1__tms_schema_full.sql)。
空库初始化当前推荐使用 [V2__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V2__tms_schema_full.sql)。
这份脚本已经整理成“最终库结构初始化版”,适合空库直接初始化,不再按 migration 逐步执行 `ALTER / RENAME`。
[V1__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V1__tms_schema_full.sql) 仅用于旧数据库迁移:它会在已有 `tms_auth_full_account` 表上新增 `password_digest`,把固定角色席位统一重置为默认口令 `Sunyard@123`,并置为强制改密。旧库不能用 `V2` 直接覆盖执行。
这份脚本已经整理成“最终库结构初始化版”,适合空库直接初始化,不再按 migration 逐步执行 `ALTER / RENAME`,也不依赖后置 `UPDATE` 修补种子数据。
如果你在 TiDB 客户端里一次性执行整份文件,出现下面这类报错:
@@ -18,7 +20,7 @@ SQL Error [8130] [HY000]: client has multi-statement capability disabled
优先用客户端的 `Run Script` / `Execute Script` 模式执行:
- [V1__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V1__tms_schema_full.sql)
- [V2__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V2__tms_schema_full.sql)
### 方式二:用“脚本执行”而不是“单次执行”
@@ -30,13 +32,13 @@ SQL Error [8130] [HY000]: client has multi-statement capability disabled
例如:
```bash
mysql -h <host> -P <port> -u <user> -p tms < /path/to/V1__tms_schema_full.sql
mysql -h <host> -P <port> -u <user> -p tms < /path/to/V2__tms_schema_full.sql
```
或者先登录后:
```sql
source /path/to/V1__tms_schema_full.sql;
source /path/to/V2__tms_schema_full.sql;
```
### 方式四:实在只能单条执行时
@@ -51,7 +53,7 @@ CREATE DATABASE IF NOT EXISTS tms
USE tms;
```
然后把 [V1__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V1__tms_schema_full.sql) 中的语句按分号逐条执行。
然后把 [V2__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V2__tms_schema_full.sql) 中的语句按分号逐条执行。
## 不推荐但可选
@@ -72,7 +74,7 @@ SET GLOBAL tidb_multi_statement_mode='ON';
- `AUDIT_ADMIN`
- `OPS_ADMIN`
默认口令均为 `12345678`,其中:
默认口令均为 `Sunyard@123`,其中:
- `KEY_ADMIN` 默认 `ACTIVE`
- 其他角色默认 `UNENABLED`
@@ -24,6 +24,7 @@ public class AuthController {
private final AuthService authService;
@PostMapping("/password-login")
@Deprecated
@Operation(summary = "口令登录", description = "按旧系统受限登录流程签发 LIMITED 会话。")
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.LOGIN, summary = "口令登录")
public ApiResponse<LoginResponse> passwordLogin(@Valid @RequestBody PasswordLoginRequest request) {
@@ -79,9 +80,23 @@ public class AuthController {
authService.changeAccountPassword(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN),
uid,
request.getCurrentPassword(),
request.getNewPassword()
request.getChangeToken(),
request.getOldPasswordDynamicDigest(),
request.getNewPasswordDigest()
);
return ApiResponse.success();
}
@PostMapping("/accounts/{uid}/change-password/prepare")
@Operation(summary = "准备修改当前登录席位口令", description = "生成改密确认令牌、旧口令挑战随机数和新口令盐值。")
@ReplayProtected
public ApiResponse<PasswordChangePrepareResponse> prepareAccountPasswordChange(
@PathVariable("uid") Integer uid,
HttpServletRequest httpRequest
) {
return ApiResponse.success(authService.prepareAccountPasswordChange(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN),
uid
));
}
}
@@ -1,32 +1,58 @@
package com.cisd.tms.modules.auth.dto;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "管理员修改账号口令请求")
public class AdminChangePasswordRequest {
@NotBlank(message = "oldPassword不能为空")
@Schema(description = "旧口令SM3摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String oldPassword;
@NotBlank(message = "oldPasswordDigest不能为空")
@JsonAlias("oldPasswordDynamicDigest")
@Schema(description = "旧口令静态摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String oldPasswordDigest;
@NotBlank(message = "newPassword不能为空")
@Schema(description = "新口令SM3摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String newPassword;
@NotBlank(message = "newPasswordDigest不能为空")
@Schema(description = "新口令静态摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String newPasswordDigest;
public String getOldPassword() {
return oldPassword;
return oldPasswordDigest;
}
public void setOldPassword(String oldPassword) {
this.oldPassword = oldPassword;
this.oldPasswordDigest = oldPassword;
}
public String getNewPassword() {
return newPassword;
return newPasswordDigest;
}
public void setNewPassword(String newPassword) {
this.newPassword = newPassword;
this.newPasswordDigest = newPassword;
}
public String getOldPasswordDynamicDigest() {
return oldPasswordDigest;
}
public void setOldPasswordDynamicDigest(String oldPasswordDynamicDigest) {
this.oldPasswordDigest = oldPasswordDynamicDigest;
}
public String getOldPasswordDigest() {
return oldPasswordDigest;
}
public void setOldPasswordDigest(String oldPasswordDigest) {
this.oldPasswordDigest = oldPasswordDigest;
}
public String getNewPasswordDigest() {
return newPasswordDigest;
}
public void setNewPasswordDigest(String newPasswordDigest) {
this.newPasswordDigest = newPasswordDigest;
}
}
@@ -7,38 +7,67 @@ import jakarta.validation.constraints.NotBlank;
@Schema(description = "修改当前认证账户口令请求")
public class ChangePasswordRequest {
@NotBlank(message = "oldPassword不能为空")
@JsonAlias("currentPassword")
@Schema(description = "旧口令SM3摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String oldPassword;
@NotBlank(message = "changeToken不能为空")
@Schema(description = "改密准备接口返回的确认令牌")
private String changeToken;
@NotBlank(message = "newPassword不能为空")
@Schema(description = "新口令SM3摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String newPassword;
@NotBlank(message = "oldPasswordDynamicDigest不能为空")
@JsonAlias({"oldPassword", "currentPassword"})
@Schema(description = "旧口令动态摘要:SM3(oldPasswordDigest + oldServerRandom)", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String oldPasswordDynamicDigest;
@NotBlank(message = "newPasswordDigest不能为空")
@JsonAlias("newPassword")
@Schema(description = "新口令静态摘要:SM3(newPasswordSalt + newPassword)", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String newPasswordDigest;
public String getOldPassword() {
return oldPassword;
return oldPasswordDynamicDigest;
}
public void setOldPassword(String oldPassword) {
this.oldPassword = oldPassword;
this.oldPasswordDynamicDigest = oldPassword;
}
@Deprecated
public String getCurrentPassword() {
return oldPassword;
return oldPasswordDynamicDigest;
}
@Deprecated
public void setCurrentPassword(String currentPassword) {
this.oldPassword = currentPassword;
this.oldPasswordDynamicDigest = currentPassword;
}
public String getNewPassword() {
return newPassword;
return newPasswordDigest;
}
public void setNewPassword(String newPassword) {
this.newPassword = newPassword;
this.newPasswordDigest = newPassword;
}
public String getChangeToken() {
return changeToken;
}
public void setChangeToken(String changeToken) {
this.changeToken = changeToken;
}
public String getOldPasswordDynamicDigest() {
return oldPasswordDynamicDigest;
}
public void setOldPasswordDynamicDigest(String oldPasswordDynamicDigest) {
this.oldPasswordDynamicDigest = oldPasswordDynamicDigest;
}
public String getNewPasswordDigest() {
return newPasswordDigest;
}
public void setNewPasswordDigest(String newPasswordDigest) {
this.newPasswordDigest = newPasswordDigest;
}
}
@@ -4,6 +4,7 @@ public class FullLoginAccountRequest {
private Integer uid;
private String password;
private String serverRandom;
public Integer getUid() {
return uid;
@@ -20,4 +21,12 @@ public class FullLoginAccountRequest {
public void setPassword(String password) {
this.password = password;
}
public String getServerRandom() {
return serverRandom;
}
public void setServerRandom(String serverRandom) {
this.serverRandom = serverRandom;
}
}
@@ -1,5 +1,6 @@
package com.cisd.tms.modules.auth.dto;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@@ -28,8 +29,10 @@ public class UkeyLoginProof {
@NotBlank(message = "loginSignature不能为空")
private String loginSignature;
@Schema(description = "当前 UKey 固定席位对应账号口令的SM3摘要,字段名保持password兼容前端", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String password;
@NotBlank(message = "passwordDynamicDigest不能为空")
@JsonAlias("password")
@Schema(description = "当前 UKey 固定席位对应账号口令动态摘要:SM3(passwordDigest + serverRandom)", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String passwordDynamicDigest;
public String getPubKey() {
return pubKey;
@@ -80,10 +83,18 @@ public class UkeyLoginProof {
}
public String getPassword() {
return password;
return passwordDynamicDigest;
}
public void setPassword(String password) {
this.password = password;
this.passwordDynamicDigest = password;
}
public String getPasswordDynamicDigest() {
return passwordDynamicDigest;
}
public void setPasswordDynamicDigest(String passwordDynamicDigest) {
this.passwordDynamicDigest = passwordDynamicDigest;
}
}
@@ -11,7 +11,7 @@ public class AuthFullAccountEntity extends BaseEntity {
private Integer uid;
private String accountName;
private String displayName;
private String passwordHash;
private String passwordDigest;
private String passwordSalt;
private String status;
private Boolean needChangePassword;
@@ -53,12 +53,12 @@ public class AuthFullAccountEntity extends BaseEntity {
this.displayName = displayName;
}
public String getPasswordHash() {
return passwordHash;
public String getPasswordDigest() {
return passwordDigest;
}
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
public void setPasswordDigest(String passwordDigest) {
this.passwordDigest = passwordDigest;
}
public String getPasswordSalt() {
@@ -5,6 +5,7 @@ import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.PasswordChangePrepareResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
@@ -27,5 +28,13 @@ public interface AuthService {
void logout(String sessionToken);
void changeAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword);
PasswordChangePrepareResponse prepareAccountPasswordChange(String sessionToken, Integer uid);
void changeAccountPassword(
String sessionToken,
Integer uid,
String changeToken,
String oldPasswordDynamicDigest,
String newPasswordDigest
);
}
@@ -16,7 +16,6 @@ import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.AuthAdminService;
import com.cisd.tms.modules.auth.service.PasswordDigestValidator;
import com.cisd.tms.modules.auth.service.PasswordHasher;
import com.cisd.tms.modules.auth.service.PasswordSaltGenerator;
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
@@ -43,7 +42,6 @@ import org.springframework.stereotype.Service;
public class AuthAdminServiceImpl implements AuthAdminService {
static final String DEFAULT_PASSWORD = "Sunyard@123";
private static final String DEFAULT_PASSWORD_DIGEST = PasswordDigestSupport.sm3Hex(DEFAULT_PASSWORD);
private final RoleAccountRepository roleAccountRepository;
private final AuthFullAccountRepository authFullAccountRepository;
@@ -52,7 +50,6 @@ public class AuthAdminServiceImpl implements AuthAdminService {
private final LmkService lmkService;
private final ObjectMapper objectMapper;
private final Clock clock;
private final PasswordSaltGenerator passwordSaltGenerator;
private void enableRoleAfterUkeyBindingIfReady(RoleCode targetRole) {
if (!hasRequiredActiveUkeyBindings(targetRole)) {
@@ -105,9 +102,8 @@ public class AuthAdminServiceImpl implements AuthAdminService {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在");
}
for (AuthFullAccountEntity account : fullAccounts) {
String newSalt = passwordSaltGenerator.nextSalt();
account.setPasswordSalt(newSalt);
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD_DIGEST, newSalt));
String salt = account.getPasswordSalt();
account.setPasswordDigest(passwordHasher.hash(PasswordDigestSupport.sm3Hex(salt + DEFAULT_PASSWORD), salt));
account.setStatus(RoleAccountStatus.ACTIVE.name());
account.setNeedChangePassword(Boolean.TRUE);
account.setPasswordChangedAt(current);
@@ -170,7 +166,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
}
AuthFullAccountEntity account = authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在"));
if (!passwordHasher.matches(oldPassword, account.getPasswordSalt(), account.getPasswordHash())) {
if (!passwordHasher.matches(oldPassword, account.getPasswordSalt(), account.getPasswordDigest())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "旧密码错误");
}
applyAdminPasswordChange(account, newPassword);
@@ -431,6 +427,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
enableRoleAfterUkeyBindingIfReady(targetRole);
return UKeySignResult.builder()
.sign(signValue)
.passwordSalt(loadAccount(targetRole.getCode(), request.getUid()).getPasswordSalt())
.backupPacket(backupPacket)
.build();
}
@@ -466,9 +463,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
}
private void applyAdminPasswordChange(AuthFullAccountEntity account, String newPassword) {
String newSalt = passwordSaltGenerator.nextSalt();
account.setPasswordSalt(newSalt);
account.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
account.setPasswordDigest(passwordHasher.hash(newPassword, account.getPasswordSalt()));
account.setNeedChangePassword(Boolean.TRUE);
account.setPasswordChangedAt(now());
account.setFailedCount(0);
@@ -484,6 +479,11 @@ public class AuthAdminServiceImpl implements AuthAdminService {
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色账号不存在"));
}
private AuthFullAccountEntity loadAccount(String roleCode, Integer uid) {
return authFullAccountRepository.findByRoleCodeAndUid(roleCode, uid)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在"));
}
private void validateRoleUid(RoleCode targetRole, Integer uid) {
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid超出角色UKey席位要求");
@@ -82,7 +82,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
account.setUid(seed.uid());
account.setAccountName(seed.accountName());
account.setDisplayName(seed.displayName());
account.setPasswordHash(seed.passwordHash());
account.setPasswordDigest(seed.passwordDigest());
account.setPasswordSalt(seed.passwordSalt());
account.setStatus(RoleAccountStatus.ACTIVE.name());
account.setNeedChangePassword(Boolean.TRUE);
@@ -120,7 +120,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
1,
"super-admin-full-01",
"超级管理员UKey席位一",
"vVPlH2UGQcXUo7NblMJMaof32um6UinF8zsckZuxe9E=",
"00718a8319e67ef6cf9fc389c14ce16784278d55439a7a30a63fa7be8e42bbdf",
"3df85c2988a5de2b9eeaa8109639641a"
),
new SeedFullAccount(
@@ -129,7 +129,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
2,
"super-admin-full-02",
"超级管理员UKey席位二",
"25e83ejpUmjU64cYAKcDV5hhqClEu6aol8YyR3Jey7E=",
"169731d7c897649a16a073cdfddf48d2e98f72f10da5cead5cbbd51f8a9ffea6",
"50de03346bb1f6afc74938acf5e88560"
),
new SeedFullAccount(
@@ -138,7 +138,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
1,
"key-admin-full-01",
"密钥管理员UKey席位",
"l65Pr7MxOvqPOe0u6ozGGZcT4raTXUgDYMQfLTINTyE=",
"6d386b2a13550befb821795dcc0e9bda3d1cffc0d565ad539de16117e28f6a81",
"073cd5fd30701edfcf033e8f501b2b43"
),
new SeedFullAccount(
@@ -147,7 +147,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
1,
"audit-admin-full-01",
"审计管理员UKey席位",
"ZRwdn3TOotyXAytO4zeSpxRm/68Rm8VcB7w7VJmZPYc=",
"72a53b0cb8dd15775516f5085c7a3ee09d65a130dc1b17e18545d24930826d0c",
"57e2616077bd8af5cb90fc9790f69573"
),
new SeedFullAccount(
@@ -156,7 +156,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
1,
"ops-admin-full-01",
"运维管理员UKey席位",
"5Wu8Mf5hC3kq4gm/62/T7pO+aZIw8Y7uYoMIztWyT1w=",
"58655960343a1efb0c1b281ce4c384ac699d01ab61c2c710d35e54b798f6e4c5",
"a6bea7601138e90a540b39c73d53df81"
)
);
@@ -171,7 +171,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
int uid,
String accountName,
String displayName,
String passwordHash,
String passwordDigest,
String passwordSalt
) {
}
@@ -10,6 +10,7 @@ import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.PasswordLoginAccountRequest;
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.PasswordChangePrepareResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginProof;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
@@ -52,6 +53,8 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -79,6 +82,7 @@ AuthServiceImpl implements AuthService {
private final SessionTokenGenerator sessionTokenGenerator;
private final PasswordSaltGenerator passwordSaltGenerator;
private final AuthPolicyService authPolicyService;
private final Map<String, PendingPasswordChange> pendingPasswordChanges = new ConcurrentHashMap<>();
@Override
public LoginResponse login(LoginRequest request) {
@@ -205,9 +209,7 @@ AuthServiceImpl implements AuthService {
}
@Override
public void changeAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword) {
PasswordDigestValidator.validate(currentPassword);
PasswordDigestValidator.validate(newPassword);
public PasswordChangePrepareResponse prepareAccountPasswordChange(String sessionToken, Integer uid) {
AuthSessionEntity session = requireActiveSession(sessionToken);
if (uid == null || !containsAuthenticatedUid(session, uid)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "当前会话未完成账号认证");
@@ -215,14 +217,60 @@ AuthServiceImpl implements AuthService {
AuthFullAccountEntity fullAccount = authFullAccountRepository.findByRoleCodeAndUid(session.getRoleCode(), uid)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "账号不存在"));
validateFullAccountStatus(fullAccount);
if (!passwordHasher.matches(currentPassword, fullAccount.getPasswordSalt(), fullAccount.getPasswordHash())) {
String changeToken = UUID.randomUUID().toString().replace("-", "");
String newPasswordSalt = passwordSaltGenerator.nextSalt();
String oldServerRandom = UUID.randomUUID().toString().replace("-", "");
pendingPasswordChanges.put(changeToken, new PendingPasswordChange(
sessionToken,
session.getRoleCode(),
uid,
newPasswordSalt,
oldServerRandom,
now().plusMinutes(IDLE_TIMEOUT_MINUTES)
));
PasswordChangePrepareResponse response = new PasswordChangePrepareResponse();
response.setChangeToken(changeToken);
response.setNewPasswordSalt(newPasswordSalt);
response.setOldServerRandom(oldServerRandom);
return response;
}
@Override
public void changeAccountPassword(
String sessionToken,
Integer uid,
String changeToken,
String oldPasswordDynamicDigest,
String newPasswordDigest
) {
PasswordDigestValidator.validate(oldPasswordDynamicDigest);
PasswordDigestValidator.validate(newPasswordDigest);
AuthSessionEntity session = requireActiveSession(sessionToken);
if (uid == null || !containsAuthenticatedUid(session, uid)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "当前会话未完成账号认证");
}
PendingPasswordChange pending = pendingPasswordChanges.remove(changeToken);
if (pending == null
|| pending.expiresAt().isBefore(now())
|| !sessionToken.equals(pending.sessionToken())
|| !session.getRoleCode().equals(pending.roleCode())
|| !uid.equals(pending.uid())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "改密确认令牌不存在或已过期");
}
AuthFullAccountEntity fullAccount = authFullAccountRepository.findByRoleCodeAndUid(session.getRoleCode(), uid)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "账号不存在"));
validateFullAccountStatus(fullAccount);
if (!PasswordDigestSupport.matchesDynamicDigest(
fullAccount.getPasswordDigest(),
pending.oldServerRandom(),
oldPasswordDynamicDigest
)) {
onFullPasswordFailed(fullAccount);
}
String salt = passwordSaltGenerator.nextSalt();
LocalDateTime current = now();
fullAccount.setPasswordSalt(salt);
fullAccount.setPasswordHash(passwordHasher.hash(newPassword, salt));
fullAccount.setPasswordSalt(pending.newPasswordSalt());
fullAccount.setPasswordDigest(passwordHasher.hash(newPasswordDigest, pending.newPasswordSalt()));
fullAccount.setFailedCount(0);
fullAccount.setLockedUntil(null);
fullAccount.setStatus(RoleAccountStatus.ACTIVE.name());
@@ -267,7 +315,7 @@ AuthServiceImpl implements AuthService {
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "账号不存在"));
validateFullAccountStatus(roleAccountSeat);
PasswordDigestValidator.validate(account.getPassword());
if (!passwordHasher.matches(account.getPassword(), roleAccountSeat.getPasswordSalt(), roleAccountSeat.getPasswordHash())) {
if (!passwordHasher.matches(account.getPassword(), roleAccountSeat.getPasswordSalt(), roleAccountSeat.getPasswordDigest())) {
onFullPasswordFailed(roleAccountSeat);
}
validatedAccounts.add(roleAccountSeat);
@@ -339,7 +387,11 @@ AuthServiceImpl implements AuthService {
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "完整账号不存在"));
validateFullAccountStatus(fullAccount);
PasswordDigestValidator.validate(account.getPassword());
if (!passwordHasher.matches(account.getPassword(), fullAccount.getPasswordSalt(), fullAccount.getPasswordHash())) {
if (!PasswordDigestSupport.matchesDynamicDigest(
fullAccount.getPasswordDigest(),
account.getServerRandom(),
account.getPassword()
)) {
onFullPasswordFailed(fullAccount);
}
validatedAccounts.add(fullAccount);
@@ -365,12 +417,23 @@ AuthServiceImpl implements AuthService {
}
FullLoginAccountRequest accountRequest = new FullLoginAccountRequest();
accountRequest.setUid(uid);
accountRequest.setPassword(proof.getPassword());
accountRequest.setPassword(proof.getPasswordDynamicDigest());
accountRequest.setServerRandom(proof.getServerRandom());
requests.add(accountRequest);
}
return requests;
}
private record PendingPasswordChange(
String sessionToken,
String roleCode,
Integer uid,
String newPasswordSalt,
String oldServerRandom,
LocalDateTime expiresAt
) {
}
private void ensureMasterKeyReady() {
MasterKeyStatus.StatusDetail status = lmkService.getMasterKeyStatus();
if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) {
@@ -1,6 +1,7 @@
package com.cisd.tms.modules.auth.service.impl;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.util.encoders.Hex;
@@ -17,4 +18,15 @@ final class PasswordDigestSupport {
digest.doFinal(result, 0);
return Hex.toHexString(result);
}
static boolean matchesDigest(String actual, String expected) {
byte[] actualBytes = (actual == null ? "" : actual).getBytes(StandardCharsets.UTF_8);
byte[] expectedBytes = (expected == null ? "" : expected).getBytes(StandardCharsets.UTF_8);
return MessageDigest.isEqual(actualBytes, expectedBytes);
}
static boolean matchesDynamicDigest(String storedPasswordDigest, String serverRandom, String actualDynamicDigest) {
return matchesDigest(sm3Hex((storedPasswordDigest == null ? "" : storedPasswordDigest)
+ (serverRandom == null ? "" : serverRandom)), actualDynamicDigest);
}
}
@@ -1,36 +1,18 @@
package com.cisd.tms.modules.auth.service.impl;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import com.cisd.tms.modules.auth.service.PasswordHasher;
import org.springframework.stereotype.Component;
@Component
public class Pbkdf2PasswordHasher implements PasswordHasher {
private static final int ITERATIONS = 65_536;
private static final int KEY_LENGTH = 256;
@Override
public String hash(String password, String salt) {
try {
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8), ITERATIONS, KEY_LENGTH);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
return Base64.getEncoder().encodeToString(keyFactory.generateSecret(spec).getEncoded());
} catch (GeneralSecurityException ex) {
throw new IllegalStateException("密码哈希计算失败", ex);
}
public String hash(String passwordDigest, String salt) {
return passwordDigest;
}
@Override
public boolean matches(String rawPassword, String salt, String expectedHash) {
byte[] actual = hash(rawPassword, salt).getBytes(StandardCharsets.UTF_8);
byte[] expected = expectedHash.getBytes(StandardCharsets.UTF_8);
return MessageDigest.isEqual(actual, expected);
public boolean matches(String actualPasswordDigest, String salt, String expectedPasswordDigest) {
return PasswordDigestSupport.matchesDigest(actualPasswordDigest, expectedPasswordDigest);
}
}
@@ -14,6 +14,11 @@ public class UKeySignResult {
*/
private String sign;
/**
* 当前角色席位口令盐值。前端写入 UKey,登录时用于计算静态口令摘要。
*/
private String passwordSalt;
/**
* 当发卡流程需要同时下发主密钥备份信息时,统一通过备份包承载。
*/
@@ -1,12 +1,14 @@
-- -----------------------------------------------------------------------------
-- TMS 全量库结构初始化脚本
-- 适用范围:空库初始化 / Flyway 首次建库
-- TMS 旧库口令摘要迁移脚本
-- 适用范围:已经存在旧认证表的数据库,从旧 password_hash 方案迁移到
-- password_digest = SM3(password_salt + 原始口令) 方案。
-- 数据库:TiDB / MySQL 8+
--
-- 说明:
-- 1. 本文件是当前仓库唯一保留的数据库初始化 SQL。
-- 2. 结构已按当前最终状态汇总,不再依赖历史 V1~V12 逐步迁移。
-- 3. 执行前请先选中目标数据库,本文件不再包含 CREATE DATABASE / USE。
-- 重要说明:
-- 1. 本文件不是空库全量初始化脚本。空库初始化请使用 V2__tms_schema_full.sql。
-- 2. 旧 password_hash 通常是不可逆摘要,无法还原用户原始口令后生成新摘要。
-- 3. 因此迁移时将固定角色席位统一重置为默认口令 Sunyard@123,并置为强制改密。
-- 4. 旧 password_hash 字段保留,便于现场回滚或排查;新代码只读取 password_digest。
-- -----------------------------------------------------------------------------
CREATE DATABASE IF NOT EXISTS TMS
@@ -15,764 +17,66 @@ CREATE DATABASE IF NOT EXISTS TMS
USE TMS;
-- -----------------------------------------------------------------------------
-- 初始化任务
-- -----------------------------------------------------------------------------
ALTER TABLE tms_auth_full_account
ADD COLUMN password_digest VARCHAR(64) NULL COMMENT '口令静态摘要:SM3(password_salt + 原始口令)' AFTER password_hash;
CREATE TABLE IF NOT EXISTS tms_init_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
task_type VARCHAR(32) NOT NULL DEFAULT 'INIT' COMMENT '任务类型:INIT/RESET',
version VARCHAR(32) NOT NULL,
status VARCHAR(32) NOT NULL,
product_type VARCHAR(16) NULL,
org_code VARCHAR(32) NULL,
mq_type VARCHAR(32) NULL,
deploy_mode VARCHAR(16) NULL,
init_plan_json LONGTEXT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_init_task_task_id (task_id)
) COMMENT='初始化任务表';
CREATE TABLE IF NOT EXISTS tms_init_task_step (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
step_no INT NOT NULL,
step_code VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
message VARCHAR(512) NULL,
command_line VARCHAR(2048) NULL,
log_path VARCHAR(512) NULL,
exit_code INT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_init_task_step_task_no (task_id, step_no),
KEY idx_tms_init_task_step_task_id (task_id)
) COMMENT='初始化任务步骤表';
-- -----------------------------------------------------------------------------
-- 资源备份与恢复
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_resource_backup_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
backup_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
product_type VARCHAR(16) NOT NULL,
mq_type VARCHAR(32) NULL,
org_code VARCHAR(32) NULL,
source_device_id VARCHAR(128) NULL,
package_name VARCHAR(255) NULL,
package_path VARCHAR(1024) NULL,
package_size BIGINT NULL,
package_hash VARCHAR(128) NULL,
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
manifest_summary_json JSON NULL,
error_code VARCHAR(64) NULL,
error_message VARCHAR(1024) NULL,
remark VARCHAR(1024) NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
start_time DATETIME(3) NULL,
finish_time DATETIME(3) NULL,
UNIQUE KEY uk_tms_resource_backup_task_task_id (task_id),
UNIQUE KEY uk_tms_resource_backup_task_backup_id (backup_id),
KEY idx_tms_resource_backup_task_status (status),
KEY idx_tms_resource_backup_task_create_time (create_time)
) COMMENT='资源备份任务表';
CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
precheck_id VARCHAR(64) NOT NULL,
backup_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
source_device_id VARCHAR(128) NULL,
source_product_type VARCHAR(16) NOT NULL,
target_product_type VARCHAR(16) NOT NULL,
package_file_id VARCHAR(64) NOT NULL,
package_path VARCHAR(1024) NULL,
staging_dir VARCHAR(1024) NULL,
runner_state_path VARCHAR(1024) NULL,
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
restore_plan_json JSON NULL,
consistency_report_json JSON NULL,
error_code VARCHAR(64) NULL,
error_message VARCHAR(1024) NULL,
remark VARCHAR(1024) NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
start_time DATETIME(3) NULL,
finish_time DATETIME(3) NULL,
UNIQUE KEY uk_tms_resource_restore_task_task_id (task_id),
UNIQUE KEY uk_tms_resource_restore_task_precheck_id (precheck_id),
KEY idx_tms_resource_restore_task_status (status),
KEY idx_tms_resource_restore_task_create_time (create_time)
) COMMENT='资源恢复任务表';
-- -----------------------------------------------------------------------------
-- 认证与设备基础表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_device_node (
id BIGINT PRIMARY KEY,
node_id VARCHAR(64) NOT NULL,
host VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_device_node_node_id (node_id)
) COMMENT='设备节点表';
CREATE TABLE IF NOT EXISTS tms_file_record (
id BIGINT PRIMARY KEY,
file_id VARCHAR(64) NOT NULL,
original_filename VARCHAR(255) NOT NULL,
storage_path VARCHAR(1024) NOT NULL,
status VARCHAR(32) NOT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_file_record_file_id (file_id)
) COMMENT='文件记录表';
-- -----------------------------------------------------------------------------
-- 设备软件版本
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_device_software_version (
id BIGINT PRIMARY KEY,
component_code VARCHAR(64) NOT NULL,
component_name VARCHAR(128) NOT NULL,
current_version VARCHAR(128) NOT NULL,
source_type VARCHAR(32) NOT NULL,
detected_at DATETIME NULL,
remarks VARCHAR(255) NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_tms_device_software_version_component_code (component_code)
) COMMENT='设备软件版本表';
INSERT INTO tms_device_software_version
(id, component_code, component_name, current_version, source_type, detected_at, remarks)
VALUES
(1001, 'RABBITMQ', '消息队列', 'V3.8.21', 'MANUAL', NOW(), '出厂预置'),
(1002, 'TLQ', '消息队列', 'V8.1.11', 'MANUAL', NOW(), '出厂预置'),
(1003, 'TIDB', '数据库', 'V8.1.0', 'MANUAL', NOW(), '出厂预置'),
(1004, 'NGINX', 'Nginx', 'V1.20', 'MANUAL', NOW(), '出厂预置'),
(1005, 'RECEIVER', '标准收发器', 'V1.3.2', 'MANUAL', NOW(), '出厂预置'),
(1006, 'TMS', 'CISD终端管理服务', 'V1.0.0', 'MANUAL', NOW(), '出厂预置')
ON DUPLICATE KEY UPDATE
component_name = VALUES(component_name),
current_version = VALUES(current_version),
source_type = VALUES(source_type),
detected_at = VALUES(detected_at),
remarks = VALUES(remarks),
update_time = CURRENT_TIMESTAMP;
-- -----------------------------------------------------------------------------
-- 升级任务
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_upgrade_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
upgrade_mode VARCHAR(16) NOT NULL,
task_type VARCHAR(32) NOT NULL,
product_type VARCHAR(16) NULL,
package_file_id VARCHAR(64) NOT NULL,
package_name VARCHAR(255) NOT NULL,
package_hash VARCHAR(128) NOT NULL,
current_version VARCHAR(64) NOT NULL,
target_version VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
verify_passed TINYINT(1) NOT NULL DEFAULT 0,
verify_message VARCHAR(1024) NULL,
manifest_json TEXT NULL,
detail_log_path VARCHAR(1024) NULL,
error_message VARCHAR(1024) NULL,
operator VARCHAR(64) NULL,
started_at DATETIME(3) NULL,
finished_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_upgrade_task_task_id (task_id),
KEY idx_tms_upgrade_task_status (status),
KEY idx_tms_upgrade_task_task_type (task_type),
KEY idx_tms_upgrade_task_create_time (create_time)
) COMMENT='升级任务表';
-- -----------------------------------------------------------------------------
-- 新认证体系
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_role_account (
id BIGINT PRIMARY KEY,
role_code VARCHAR(64) NOT NULL,
display_name VARCHAR(128) NOT NULL,
required_ukey_count INT NOT NULL,
status VARCHAR(32) NOT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_role_account_role_code (role_code)
) COMMENT='角色账号表';
CREATE TABLE IF NOT EXISTS tms_role_ukey_binding (
id BIGINT PRIMARY KEY,
role_code VARCHAR(64) NOT NULL,
uid INT NOT NULL,
ukey_serial VARCHAR(128) NOT NULL,
ukey_pubkey TEXT NOT NULL,
issuer_sign TEXT NULL,
status VARCHAR(32) NOT NULL,
bound_at DATETIME(3) NOT NULL,
unbound_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_role_ukey_binding_role_uid_serial (role_code, uid, ukey_serial),
KEY idx_tms_role_ukey_binding_role_uid_status (role_code, uid, status),
KEY idx_tms_role_ukey_binding_role_status (role_code, status)
) COMMENT='角色UKey绑定表';
CREATE TABLE IF NOT EXISTS tms_auth_full_account (
id BIGINT PRIMARY KEY,
role_code VARCHAR(64) NOT NULL,
uid INT NOT NULL,
account_name VARCHAR(64) NOT NULL,
display_name VARCHAR(128) NOT NULL,
password_hash VARCHAR(256) NOT NULL,
password_salt VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
need_change_password TINYINT(1) NOT NULL DEFAULT 1,
password_changed_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
failed_count INT NOT NULL DEFAULT 0,
locked_until DATETIME(3) NULL,
last_login_at DATETIME(3) NULL,
last_active_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_auth_full_account_role_uid (role_code, uid),
UNIQUE KEY uk_tms_auth_full_account_name (account_name),
KEY idx_tms_auth_full_account_role_code (role_code)
) COMMENT='完整认证账号表';
CREATE TABLE IF NOT EXISTS tms_auth_session (
id BIGINT PRIMARY KEY,
session_token VARCHAR(128) NOT NULL,
role_code VARCHAR(64) NOT NULL,
authenticated_principals_json JSON NULL,
auth_method VARCHAR(32) NOT NULL,
auth_level VARCHAR(32) NOT NULL,
issued_at DATETIME(3) NOT NULL,
last_active_at DATETIME(3) NOT NULL,
expires_at DATETIME(3) NOT NULL,
logout_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_auth_session_token (session_token),
KEY idx_tms_auth_session_role_code (role_code)
) COMMENT='认证会话表';
-- -----------------------------------------------------------------------------
-- 防重放与安全事件
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_replay_nonce (
id BIGINT PRIMARY KEY,
scope VARCHAR(32) NOT NULL COMMENT '防重放作用域:OPENAPI / INTERNAL_API',
principal_id VARCHAR(128) NOT NULL COMMENT '主体标识:appId / sessionToken',
nonce VARCHAR(128) NOT NULL,
request_timestamp BIGINT NOT NULL,
request_method VARCHAR(16) NULL,
request_path VARCHAR(256) NULL,
body_hash VARCHAR(128) NULL,
remote_ip VARCHAR(64) NULL,
user_agent VARCHAR(512) NULL,
expires_at DATETIME(3) NOT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_replay_nonce_scope_principal_nonce (scope, principal_id, nonce),
KEY idx_tms_replay_nonce_expires_at (expires_at),
KEY idx_tms_replay_nonce_principal_time (principal_id, create_time)
) COMMENT='防重放nonce表';
CREATE TABLE IF NOT EXISTS tms_security_event (
id BIGINT PRIMARY KEY,
event_type VARCHAR(64) NOT NULL,
scope VARCHAR(32) NOT NULL,
principal_id VARCHAR(128) NULL,
request_method VARCHAR(16) NULL,
request_path VARCHAR(256) NULL,
nonce VARCHAR(128) NULL,
remote_ip VARCHAR(64) NULL,
detail_json JSON NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
KEY idx_tms_security_event_event_type (event_type),
KEY idx_tms_security_event_scope_principal (scope, principal_id)
) COMMENT='安全事件表';
INSERT IGNORE INTO tms_role_account (
id,
role_code,
display_name,
required_ukey_count,
status,
create_time,
update_time
)
VALUES
(
11001,
'SUPER_ADMIN',
'超级管理员',
2,
'ACTIVE',
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
11002,
'KEY_ADMIN',
'密钥管理员',
1,
'UNENABLED',
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
11003,
'AUDIT_ADMIN',
'审计管理员',
1,
'UNENABLED',
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
11004,
'OPS_ADMIN',
'运维管理员',
1,
'UNENABLED',
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
);
INSERT IGNORE INTO tms_auth_full_account (
id,
role_code,
uid,
account_name,
display_name,
password_hash,
password_salt,
status,
need_change_password,
password_changed_at,
failed_count,
locked_until,
last_login_at,
last_active_at,
create_time,
update_time
)
VALUES
(
12101,
'SUPER_ADMIN',
1,
'super-admin-full-01',
'超级管理员UKey席位一',
'vVPlH2UGQcXUo7NblMJMaof32um6UinF8zsckZuxe9E=',
'3df85c2988a5de2b9eeaa8109639641a',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12102,
'SUPER_ADMIN',
2,
'super-admin-full-02',
'超级管理员UKey席位二',
'25e83ejpUmjU64cYAKcDV5hhqClEu6aol8YyR3Jey7E=',
'50de03346bb1f6afc74938acf5e88560',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12103,
'KEY_ADMIN',
1,
'key-admin-full-01',
'密钥管理员UKey席位',
'l65Pr7MxOvqPOe0u6ozGGZcT4raTXUgDYMQfLTINTyE=',
'073cd5fd30701edfcf033e8f501b2b43',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12104,
'AUDIT_ADMIN',
1,
'audit-admin-full-01',
'审计管理员UKey席位',
'ZRwdn3TOotyXAytO4zeSpxRm/68Rm8VcB7w7VJmZPYc=',
'57e2616077bd8af5cb90fc9790f69573',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12105,
'OPS_ADMIN',
1,
'ops-admin-full-01',
'运维管理员UKey席位',
'5Wu8Mf5hC3kq4gm/62/T7pO+aZIw8Y7uYoMIztWyT1w=',
'a6bea7601138e90a540b39c73d53df81',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
);
-- -----------------------------------------------------------------------------
-- 兼容旧库默认口令摘要升级
-- -----------------------------------------------------------------------------
-- 旧版本默认账号保存的是 PBKDF2('Sunyard@123', password_salt)。
-- 当前版本前端提交 password=SM3('Sunyard@123'),因此默认账号统一重置为
-- 固定 salt + PBKDF2(SM3('Sunyard@123'), salt),便于旧库直接切换到摘要口径。
-- 默认口令摘要:SM3(password_salt + 'Sunyard@123')。
-- 迁移会把固定席位盐值同步为后端当前标准盐值,后续 issue-sign 会返回该盐值供前端写入 UKey。
UPDATE tms_auth_full_account
SET password_hash = 'vVPlH2UGQcXUo7NblMJMaof32um6UinF8zsckZuxe9E=',
password_salt = '3df85c2988a5de2b9eeaa8109639641a',
SET password_salt = '3df85c2988a5de2b9eeaa8109639641a',
password_digest = '00718a8319e67ef6cf9fc389c14ce16784278d55439a7a30a63fa7be8e42bbdf',
need_change_password = 1,
failed_count = 0,
locked_until = NULL,
password_changed_at = CURRENT_TIMESTAMP(3),
update_time = CURRENT_TIMESTAMP(3)
WHERE role_code = 'SUPER_ADMIN'
AND uid = 1;
UPDATE tms_auth_full_account
SET password_hash = '25e83ejpUmjU64cYAKcDV5hhqClEu6aol8YyR3Jey7E=',
password_salt = '50de03346bb1f6afc74938acf5e88560',
SET password_salt = '50de03346bb1f6afc74938acf5e88560',
password_digest = '169731d7c897649a16a073cdfddf48d2e98f72f10da5cead5cbbd51f8a9ffea6',
need_change_password = 1,
failed_count = 0,
locked_until = NULL,
password_changed_at = CURRENT_TIMESTAMP(3),
update_time = CURRENT_TIMESTAMP(3)
WHERE role_code = 'SUPER_ADMIN'
AND uid = 2;
UPDATE tms_auth_full_account
SET password_hash = 'l65Pr7MxOvqPOe0u6ozGGZcT4raTXUgDYMQfLTINTyE=',
password_salt = '073cd5fd30701edfcf033e8f501b2b43',
SET password_salt = '073cd5fd30701edfcf033e8f501b2b43',
password_digest = '6d386b2a13550befb821795dcc0e9bda3d1cffc0d565ad539de16117e28f6a81',
need_change_password = 1,
failed_count = 0,
locked_until = NULL,
password_changed_at = CURRENT_TIMESTAMP(3),
update_time = CURRENT_TIMESTAMP(3)
WHERE role_code = 'KEY_ADMIN'
AND uid = 1;
UPDATE tms_auth_full_account
SET password_hash = 'ZRwdn3TOotyXAytO4zeSpxRm/68Rm8VcB7w7VJmZPYc=',
password_salt = '57e2616077bd8af5cb90fc9790f69573',
SET password_salt = '57e2616077bd8af5cb90fc9790f69573',
password_digest = '72a53b0cb8dd15775516f5085c7a3ee09d65a130dc1b17e18545d24930826d0c',
need_change_password = 1,
failed_count = 0,
locked_until = NULL,
password_changed_at = CURRENT_TIMESTAMP(3),
update_time = CURRENT_TIMESTAMP(3)
WHERE role_code = 'AUDIT_ADMIN'
AND uid = 1;
UPDATE tms_auth_full_account
SET password_hash = '5Wu8Mf5hC3kq4gm/62/T7pO+aZIw8Y7uYoMIztWyT1w=',
password_salt = 'a6bea7601138e90a540b39c73d53df81',
SET password_salt = 'a6bea7601138e90a540b39c73d53df81',
password_digest = '58655960343a1efb0c1b281ce4c384ac699d01ab61c2c710d35e54b798f6e4c5',
need_change_password = 1,
failed_count = 0,
locked_until = NULL,
password_changed_at = CURRENT_TIMESTAMP(3),
update_time = CURRENT_TIMESTAMP(3)
WHERE role_code = 'OPS_ADMIN'
AND uid = 1;
-- -----------------------------------------------------------------------------
-- IP白名单表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_access_whitelist (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
ip VARCHAR(32) NOT NULL,
mask VARCHAR(10) NOT NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
) COMMENT='访问白名单表';
-- -----------------------------------------------------------------------------
-- Cert module tables
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_key_entity (
id BIGINT PRIMARY KEY,
org_code VARCHAR(64) NOT NULL COMMENT 'organization code',
org_name VARCHAR(255) NOT NULL COMMENT 'organization name',
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 / RSA',
key_len INT NOT NULL COMMENT 'SM2 fixed 256 in V1',
key_idx INT NOT NULL COMMENT 'card key index, allowed range [2..8]',
backup_data TEXT NOT NULL COMMENT 'SDFE_BackupUserKey output',
public_key VARCHAR(500) NOT NULL COMMENT 'public key, Base64/PEM',
p10_data VARCHAR(2000) NULL COMMENT 'PKCS#10 PEM content',
subject_dn VARCHAR(500) NULL COMMENT 'DN used when generating P10',
cert_mode VARCHAR(16) NULL COMMENT 'SINGLE / DOUBLE',
status VARCHAR(32) NOT NULL COMMENT 'CREATED / P10_GENERATED / CERT_IMPORTED',
allow_ips VARCHAR(500) NULL COMMENT 'allowed source IP list',
sign_data VARCHAR(512) NULL COMMENT 'reserved, not used in V1',
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_key_entity_key_idx (key_idx),
KEY idx_tms_key_entity_org_code (org_code),
KEY idx_tms_key_entity_algo (algo_type),
KEY idx_tms_key_entity_status (status),
KEY idx_tms_key_entity_create_time (create_time)
) COMMENT='密钥实体表';
CREATE TABLE IF NOT EXISTS tms_certificate (
id BIGINT PRIMARY KEY,
entity_id BIGINT NULL COMMENT 'required when cert_type=ENTITY',
cert_type VARCHAR(16) NOT NULL COMMENT 'ENTITY / USER',
subject_dn VARCHAR(500) NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
cert_data VARCHAR(2000) NOT NULL COMMENT 'certificate PEM content',
valid_from DATETIME(3) NOT NULL,
valid_to DATETIME(3) NOT NULL,
import_time DATETIME(3) NOT NULL,
sign_data VARCHAR(512) NULL COMMENT 'reserved, not used in V1',
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_certificate_fingerprint (fingerprint),
KEY idx_tms_certificate_entity_id (entity_id),
KEY idx_tms_certificate_cert_type (cert_type),
KEY idx_tms_certificate_issuer_dn (issuer_dn(512)),
KEY idx_tms_certificate_valid_to (valid_to),
KEY idx_tms_certificate_import_time (import_time)
) COMMENT='证书表';
CREATE TABLE IF NOT EXISTS tms_trusted_cert (
id BIGINT PRIMARY KEY,
alias VARCHAR(128) NULL,
subject_dn VARCHAR(1024) NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
cert_data VARCHAR(2000) NOT NULL COMMENT 'certificate PEM content',
ca_level TINYINT NOT NULL COMMENT '0:Root, 1:Intermediate',
valid_from DATETIME(3) NOT NULL,
valid_to DATETIME(3) NOT NULL,
import_time DATETIME(3) NOT NULL,
sign_data VARCHAR(512) NULL COMMENT 'reserved, not used in V1',
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_trusted_cert_fingerprint (fingerprint),
KEY idx_tms_trusted_cert_subject_dn (subject_dn(512)),
KEY idx_tms_trusted_cert_issuer_dn (issuer_dn(512)),
KEY idx_tms_trusted_cert_valid_to (valid_to)
) COMMENT='可信证书表';
CREATE TABLE IF NOT EXISTS tms_certificate_crl (
id BIGINT PRIMARY KEY,
issuer_dn VARCHAR(1024) NOT NULL,
crl_number VARCHAR(128) NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
crl_data MEDIUMTEXT NOT NULL COMMENT 'reserved legacy CRL PEM content; large CRLs are not stored inline',
this_update DATETIME(3) NOT NULL,
next_update DATETIME(3) NULL,
revoked_count INT NOT NULL,
import_time DATETIME(3) NOT NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_certificate_crl_fingerprint (fingerprint),
KEY idx_tms_certificate_crl_issuer_dn (issuer_dn(512)),
KEY idx_tms_certificate_crl_this_update (this_update),
KEY idx_tms_certificate_crl_next_update (next_update)
) COMMENT='证书CRL表';
CREATE TABLE IF NOT EXISTS tms_certificate_crl_revoked (
id BIGINT PRIMARY KEY,
crl_id BIGINT NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
subject_dn VARCHAR(1024) NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
crl_number VARCHAR(128) NULL,
revocation_time DATETIME(3) NOT NULL,
revocation_reason VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
KEY idx_tms_certificate_crl_revoked_crl_id (crl_id),
KEY idx_tms_certificate_crl_revoked_issuer_serial (issuer_dn(512), serial_number),
KEY idx_tms_certificate_crl_revoked_subject_dn (subject_dn(512)),
KEY idx_tms_certificate_crl_revoked_revocation_time (revocation_time)
) COMMENT='证书CRL吊销明细表';
CREATE TABLE IF NOT EXISTS tms_certificate_crl_import_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
original_filename VARCHAR(255) NULL,
file_size BIGINT NULL,
status VARCHAR(32) NOT NULL COMMENT 'PENDING/RUNNING/SUCCESS/FAILED',
fingerprint VARCHAR(128) NULL COMMENT 'SHA-256 fingerprint after successful parse',
crl_id BIGINT NULL,
revoked_count INT NOT NULL DEFAULT 0,
error_message VARCHAR(512) NULL,
started_at DATETIME(3) NULL,
finished_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_certificate_crl_import_task_task_id (task_id),
KEY idx_tms_certificate_crl_import_task_status (status),
KEY idx_tms_certificate_crl_import_task_create_time (create_time)
) COMMENT='证书CRL导入任务表';
-- -----------------------------------------------------------------------------
-- 日志表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_operation_audit_log (
id BIGINT PRIMARY KEY,
log_id VARCHAR(64) NOT NULL COMMENT '日志唯一标识',
operator_role_code VARCHAR(64) NULL COMMENT '操作角色编码',
operator_auth_level VARCHAR(32) NULL COMMENT '操作时认证等级',
module_code VARCHAR(32) NOT NULL COMMENT '业务模块',
action_type VARCHAR(32) NOT NULL COMMENT '操作动作',
remote_ip VARCHAR(64) NULL COMMENT '来源IP',
operation_result VARCHAR(16) NOT NULL COMMENT '操作结果: SUCCESS/FAILED/DENIED',
summary VARCHAR(512) NOT NULL COMMENT '操作摘要',
error_message VARCHAR(1024) NULL COMMENT '失败原因',
audit_status VARCHAR(16) NOT NULL COMMENT '审计状态: PENDING/REVIEWED',
audit_result VARCHAR(16) NULL COMMENT '审计结果: PASS/REJECT/NEED_VERIFY',
audit_comment VARCHAR(1024) NULL COMMENT '审计意见',
audited_by VARCHAR(64) NULL COMMENT '审计人',
audited_at DATETIME(3) NULL COMMENT '审计时间',
payload_hash VARCHAR(128) NOT NULL COMMENT 'SM3摘要',
sign_value TEXT NOT NULL COMMENT 'SM2签名值',
occurred_at DATETIME(3) NOT NULL COMMENT '操作发生时间',
create_time DATETIME(3) NOT NULL COMMENT '落库时间',
UNIQUE KEY uk_tms_operation_audit_log_log_id (log_id),
KEY idx_tms_operation_audit_log_role_code (operator_role_code),
KEY idx_tms_operation_audit_log_module_code (module_code),
KEY idx_tms_operation_audit_log_action_type (action_type),
KEY idx_tms_operation_audit_log_result (operation_result),
KEY idx_tms_operation_audit_log_audit_status (audit_status),
KEY idx_tms_operation_audit_log_occurred_at (occurred_at)
) COMMENT='操作审计日志表';
-- -----------------------------------------------------------------------------
-- 日志备份记录表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_log_backup_record (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
record_id VARCHAR(64) NOT NULL COMMENT '备份记录ID',
file_name VARCHAR(255) NOT NULL COMMENT '备份文件名',
backup_type VARCHAR(32) NOT NULL COMMENT '备份类型(如:AUTO/MANUAL)',
file_path VARCHAR(500) NOT NULL COMMENT '文件存储路径',
file_hash VARCHAR(128) NOT NULL COMMENT '文件哈希值',
file_sign VARCHAR(256) DEFAULT NULL COMMENT '文件签名',
backup_result VARCHAR(32) NOT NULL COMMENT '备份结果(SUCCESS/FAILED)',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
sign_data TEXT DEFAULT NULL COMMENT '其他字段签名',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_record_id` (`record_id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_backup_type` (`backup_type`)
) COMMENT='日志备份记录表';
-- -----------------------------------------------------------------------------
-- 日志备份配置表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_backup_config (
id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
enable tinyint(4) NOT NULL DEFAULT '0' COMMENT '启用状态 1:启用 0:禁用',
cron_exp varchar(64) DEFAULT NULL COMMENT 'Cron表达式',
retention_count int(11) DEFAULT '1' COMMENT '保留份数',
last_backup_time datetime DEFAULT NULL COMMENT '上次执行时间',
sign_data varchar(255) NOT NULL COMMENT '其他字段的签名值',
PRIMARY KEY (`id`)
) COMMENT='日志备份配置表';
CREATE TABLE IF NOT EXISTS tms_master_key_activate (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
ever_initialized BOOLEAN NOT NULL,
activation_status BOOLEAN NOT NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
) COMMENT='主密钥激活状态表';
INSERT IGNORE INTO tms_master_key_activate (
id,
ever_initialized,
activation_status
) VALUES (1, true, true);
ALTER TABLE tms_access_whitelist ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_auth_audit_log ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_auth_challenge ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_auth_full_account ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_auth_session ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_backup_config ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_certificate ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_certificate_crl ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_certificate_crl_import_task ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_certificate_crl_revoked ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_device_node ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_device_software_version ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_file_record ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_init_task ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_init_task_step ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_key_entity ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_log_backup_record ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_master_key_activate ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_operation_audit_log ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_replay_nonce ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_resource_backup_task ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_resource_restore_task ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_role_account ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_role_ukey_binding ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_security_event ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_trusted_cert ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_upgrade_task ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
-- 如果这里失败,说明旧库中存在不属于上述固定席位的认证账号,需要先明确其迁移策略。
ALTER TABLE tms_auth_full_account
MODIFY COLUMN password_digest VARCHAR(64) NOT NULL COMMENT '口令静态摘要:SM3(password_salt + 原始口令)';
@@ -247,7 +247,7 @@ CREATE TABLE IF NOT EXISTS tms_auth_full_account (
uid INT NOT NULL,
account_name VARCHAR(64) NOT NULL,
display_name VARCHAR(128) NOT NULL,
password_hash VARCHAR(256) NOT NULL,
password_digest VARCHAR(64) NOT NULL,
password_salt VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
need_change_password TINYINT(1) NOT NULL DEFAULT 1,
@@ -376,7 +376,7 @@ INSERT IGNORE INTO tms_auth_full_account (
uid,
account_name,
display_name,
password_hash,
password_digest,
password_salt,
status,
need_change_password,
@@ -395,7 +395,7 @@ VALUES
1,
'super-admin-full-01',
'超级管理员UKey席位一',
'vVPlH2UGQcXUo7NblMJMaof32um6UinF8zsckZuxe9E=',
'00718a8319e67ef6cf9fc389c14ce16784278d55439a7a30a63fa7be8e42bbdf',
'3df85c2988a5de2b9eeaa8109639641a',
'ACTIVE',
1,
@@ -413,7 +413,7 @@ VALUES
2,
'super-admin-full-02',
'超级管理员UKey席位二',
'25e83ejpUmjU64cYAKcDV5hhqClEu6aol8YyR3Jey7E=',
'169731d7c897649a16a073cdfddf48d2e98f72f10da5cead5cbbd51f8a9ffea6',
'50de03346bb1f6afc74938acf5e88560',
'ACTIVE',
1,
@@ -431,7 +431,7 @@ VALUES
1,
'key-admin-full-01',
'密钥管理员UKey席位',
'l65Pr7MxOvqPOe0u6ozGGZcT4raTXUgDYMQfLTINTyE=',
'6d386b2a13550befb821795dcc0e9bda3d1cffc0d565ad539de16117e28f6a81',
'073cd5fd30701edfcf033e8f501b2b43',
'ACTIVE',
1,
@@ -449,7 +449,7 @@ VALUES
1,
'audit-admin-full-01',
'审计管理员UKey席位',
'ZRwdn3TOotyXAytO4zeSpxRm/68Rm8VcB7w7VJmZPYc=',
'72a53b0cb8dd15775516f5085c7a3ee09d65a130dc1b17e18545d24930826d0c',
'57e2616077bd8af5cb90fc9790f69573',
'ACTIVE',
1,
@@ -467,7 +467,7 @@ VALUES
1,
'ops-admin-full-01',
'运维管理员UKey席位',
'5Wu8Mf5hC3kq4gm/62/T7pO+aZIw8Y7uYoMIztWyT1w=',
'58655960343a1efb0c1b281ce4c384ac699d01ab61c2c710d35e54b798f6e4c5',
'a6bea7601138e90a540b39c73d53df81',
'ACTIVE',
1,
@@ -10,7 +10,7 @@
<result property="uid" column="uid"/>
<result property="accountName" column="account_name"/>
<result property="displayName" column="display_name"/>
<result property="passwordHash" column="password_hash"/>
<result property="passwordDigest" column="password_digest"/>
<result property="passwordSalt" column="password_salt"/>
<result property="status" column="status"/>
<result property="needChangePassword" column="need_change_password"/>
@@ -29,7 +29,7 @@
uid,
account_name,
display_name,
password_hash,
password_digest,
password_salt,
status,
need_change_password,
@@ -52,7 +52,7 @@
uid,
account_name,
display_name,
password_hash,
password_digest,
password_salt,
status,
need_change_password,
@@ -73,7 +73,7 @@
uid,
account_name,
display_name,
password_hash,
password_digest,
password_salt,
status,
need_change_password,
@@ -3,6 +3,8 @@ package com.cisd.tms;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -19,8 +21,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@AutoConfigureMockMvc
class TmsApplicationTests {
private static final String DEFAULT_PASSWORD_DIGEST =
"fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b";
private static final String DEFAULT_PASSWORD = "Sunyard@123";
private static final Pattern AUTH_FULL_ACCOUNT_ROW = Pattern.compile(
"\\(\\s*1210[1-5],.*?'([^']+)'\\s*,\\s*'([0-9a-f]{32})'",
@@ -114,16 +115,23 @@ class TmsApplicationTests {
Matcher matcher = AUTH_FULL_ACCOUNT_ROW.matcher(sql);
java.util.Set<String> salts = new java.util.HashSet<>();
com.cisd.tms.modules.auth.service.impl.Pbkdf2PasswordHasher hasher =
new com.cisd.tms.modules.auth.service.impl.Pbkdf2PasswordHasher();
int rows = 0;
while (matcher.find()) {
rows++;
String hash = matcher.group(1);
String digest = matcher.group(1);
String salt = matcher.group(2);
org.junit.jupiter.api.Assertions.assertTrue(salts.add(salt), "duplicate salt: " + salt);
org.junit.jupiter.api.Assertions.assertTrue(hasher.matches(DEFAULT_PASSWORD_DIGEST, salt, hash), "hash mismatch for salt: " + salt);
org.junit.jupiter.api.Assertions.assertEquals(sm3Hex(salt + DEFAULT_PASSWORD), digest, "digest mismatch for salt: " + salt);
}
org.junit.jupiter.api.Assertions.assertEquals(5, rows);
}
private static String sm3Hex(String value) {
byte[] input = value.getBytes(StandardCharsets.UTF_8);
SM3Digest digest = new SM3Digest();
digest.update(input, 0, input.length);
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return Hex.toHexString(result);
}
}
@@ -32,6 +32,8 @@ class AuthControllerTest {
"0fffff81e971fa3f09107abf77931463fc0710bfb8962efeae3d5654b073bb0c";
private static final String DIGEST_ABC1234 =
"6b66c27d356b7fef3e2c2a986043031a410bb564205f22525ba022591671a7e5";
private static final String DYNAMIC_DIGEST =
"55f6d97a52f5f3f27541e3137297a8f807d1ffdfb6596cdd562c8989bc58dc7f";
@Test
void shouldLoginWithPasswordPayload() throws Exception {
@@ -118,7 +120,7 @@ class AuthControllerTest {
{
"pubKey": "PUB-1",
"uid": 1,
"password": "%s",
"passwordDynamicDigest": "%s",
"serverRandom": "RB-1",
"issueSignature": "ISSUE-1",
"loginPayload": "LOGIN-DATA-1",
@@ -127,7 +129,7 @@ class AuthControllerTest {
{
"pubKey": "PUB-2",
"uid": 2,
"password": "%s",
"passwordDynamicDigest": "%s",
"serverRandom": "RB-2",
"issueSignature": "ISSUE-2",
"loginPayload": "LOGIN-DATA-2",
@@ -135,7 +137,7 @@ class AuthControllerTest {
}
]
}
""".formatted(DIGEST_12345678, DIGEST_ABC1234)))
""".formatted(DYNAMIC_DIGEST, DYNAMIC_DIGEST)))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"token\":\"token-ukey-001\"")))
.andExpect(content().string(containsString("\"authLevel\":\"FULL\"")));
@@ -220,14 +222,15 @@ class AuthControllerTest {
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"oldPassword": "%s",
"newPassword": "%s"
"changeToken": "change-token-001",
"oldPasswordDynamicDigest": "%s",
"newPasswordDigest": "%s"
}
""".formatted(DIGEST_12345678, DIGEST_ABC1234)))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"success\":true")));
Mockito.verify(authService).changeAccountPassword("token-change-001", 1, DIGEST_12345678, DIGEST_ABC1234);
Mockito.verify(authService).changeAccountPassword("token-change-001", 1, "change-token-001", DIGEST_12345678, DIGEST_ABC1234);
}
@Test
@@ -246,8 +249,8 @@ class AuthControllerTest {
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"oldPassword": "%s",
"newPassword": "%s"
"oldPasswordDigest": "%s",
"newPasswordDigest": "%s"
}
""".formatted(DIGEST_12345678, DIGEST_ABC1234)))
.andExpect(status().isOk())
@@ -303,7 +306,7 @@ class AuthControllerTest {
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MasterKeyBackupPacket packet = new MasterKeyBackupPacket();
packet.setPacketIndex(1);
UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-001").backupPacket(packet).build();
UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-001").passwordSalt("SALT-001").backupPacket(packet).build();
Mockito.when(authAdminService.issueUkeyBindingSign(
ArgumentMatchers.isNull(),
ArgumentMatchers.isNull(),
@@ -328,6 +331,7 @@ class AuthControllerTest {
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-001\"")))
.andExpect(content().string(containsString("\"passwordSalt\":\"SALT-001\"")))
.andExpect(content().string(containsString("\"packetIndex\":1")));
}
@@ -335,7 +339,7 @@ class AuthControllerTest {
void shouldIssueNormalAdminUkeyBindingSignThroughAdminEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-KEY-001").build();
UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-KEY-001").passwordSalt("SALT-KEY").build();
Mockito.when(authAdminService.issueUkeyBindingSign(
ArgumentMatchers.eq("SUPER_ADMIN"),
ArgumentMatchers.eq("FULL"),
@@ -360,7 +364,8 @@ class AuthControllerTest {
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-KEY-001\"")));
.andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-KEY-001\"")))
.andExpect(content().string(containsString("\"passwordSalt\":\"SALT-KEY\"")));
}
@Test
@@ -34,7 +34,6 @@ import org.mockito.Mockito;
class AuthAdminServiceTest {
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T02:00:00Z"), ZoneOffset.UTC);
private static final String DEFAULT_PASSWORD_DIGEST = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b";
private static final String DIGEST_OLD1234 = "5237f20185252395f6ce351f8e742085d6ab1207ddda94b085b409e41026fc04";
private static final String DIGEST_NEW1234 = "6b66c27d356b7fef3e2c2a986043031a410bb564205f22525ba022591671a7e5";
private static final String DIGEST_BAD_PASSWORD = "17cb3cec9c337826293158dd5623c75268a2eee06123ba53b9aef5b790c04e51";
@@ -52,10 +51,10 @@ class AuthAdminServiceTest {
AuthFullAccountEntity first = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow();
AuthFullAccountEntity second = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow();
Assertions.assertEquals("salt-reset-1", first.getPasswordSalt());
Assertions.assertEquals("HASH:" + DEFAULT_PASSWORD_DIGEST + ":salt-reset-1", first.getPasswordHash());
Assertions.assertEquals("salt-reset-2", second.getPasswordSalt());
Assertions.assertEquals("HASH:" + DEFAULT_PASSWORD_DIGEST + ":salt-reset-2", second.getPasswordHash());
Assertions.assertEquals("OLD-SALT-1", first.getPasswordSalt());
Assertions.assertEquals("434685979fe125d142bfa1568d61ca1c6349bb54befb766617a1502d07bdc6c3", first.getPasswordDigest());
Assertions.assertEquals("OLD-SALT-2", second.getPasswordSalt());
Assertions.assertEquals("dd6eae63d8a5776a4ed2b01cfac3d0b31ae15f331dfa507cd833da3ea5b504e4", second.getPasswordDigest());
Assertions.assertTrue(Boolean.TRUE.equals(first.getNeedChangePassword()));
Assertions.assertTrue(Boolean.TRUE.equals(second.getNeedChangePassword()));
}
@@ -65,7 +64,7 @@ class AuthAdminServiceTest {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE));
accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", "HASH:" + DIGEST_OLD1234 + ":SALT-OLD", "SALT-OLD"));
accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", DIGEST_OLD1234, "SALT-OLD"));
service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class))
.changeAccountPassword(
@@ -78,8 +77,8 @@ class AuthAdminServiceTest {
);
AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals("salt-reset-1", changed.getPasswordSalt());
Assertions.assertEquals("HASH:" + DIGEST_NEW1234 + ":salt-reset-1", changed.getPasswordHash());
Assertions.assertEquals("SALT-OLD", changed.getPasswordSalt());
Assertions.assertEquals(DIGEST_NEW1234, changed.getPasswordDigest());
Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword()));
}
@@ -88,7 +87,7 @@ class AuthAdminServiceTest {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE));
accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", "HASH:" + DIGEST_OLD1234 + ":SALT-OLD", "SALT-OLD"));
accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", DIGEST_OLD1234, "SALT-OLD"));
BizException exception = Assertions.assertThrows(BizException.class,
() -> service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class))
@@ -132,6 +131,7 @@ class AuthAdminServiceTest {
.issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto);
Assertions.assertEquals("ISSUE-SIGN-REBIND", result.getSign());
Assertions.assertEquals("SALT-OLD", result.getPasswordSalt());
Assertions.assertNotNull(result.getBackupPacket());
List<RoleUkeyBindingEntity> activeUidBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1);
Assertions.assertEquals(2, activeUidBindings.size());
@@ -184,6 +184,7 @@ class AuthAdminServiceTest {
.issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.KEY_ADMIN.getCode(), dto);
Assertions.assertEquals("ISSUE-SIGN-NEW", result.getSign());
Assertions.assertEquals("SALT-OLD", result.getPasswordSalt());
Assertions.assertNull(result.getBackupPacket());
List<RoleUkeyBindingEntity> activeBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1);
Assertions.assertEquals(1, activeBindings.size());
@@ -394,8 +395,7 @@ class AuthAdminServiceTest {
new FakePasswordHasher(),
lmkService,
new ObjectMapper(),
FIXED_CLOCK,
new FixedSaltSupplier("salt-reset-1", "salt-reset-2", "salt-reset-3")
FIXED_CLOCK
);
}
@@ -416,7 +416,7 @@ class AuthAdminServiceTest {
entity.setUid(uid);
entity.setAccountName(accountName);
entity.setDisplayName(accountName);
entity.setPasswordHash(passwordHash);
entity.setPasswordDigest(passwordHash);
entity.setPasswordSalt(passwordSalt);
entity.setStatus(RoleAccountStatus.ACTIVE.name());
entity.setNeedChangePassword(Boolean.FALSE);
@@ -440,29 +440,12 @@ class AuthAdminServiceTest {
private static class FakePasswordHasher implements PasswordHasher {
@Override
public String hash(String password, String salt) {
return "HASH:" + password + ":" + salt;
return password;
}
@Override
public boolean matches(String rawPassword, String salt, String expectedHash) {
return hash(rawPassword, salt).equals(expectedHash);
}
}
private static class FixedSaltSupplier implements PasswordSaltGenerator {
private final List<String> salts;
private int index = 0;
private FixedSaltSupplier(String... salts) {
this.salts = List.of(salts);
}
@Override
public String nextSalt() {
if (index >= salts.size()) {
return salts.get(salts.size() - 1);
}
return salts.get(index++);
return rawPassword.equals(expectedHash);
}
}
@@ -22,7 +22,8 @@ import org.mockito.Mockito;
class AuthSecurityResetServiceTest {
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-05-13T08:00:00Z"), ZoneOffset.UTC);
private static final String FIRST_SUPER_ADMIN_PASSWORD_HASH = "vVPlH2UGQcXUo7NblMJMaof32um6UinF8zsckZuxe9E=";
private static final String FIRST_SUPER_ADMIN_PASSWORD_DIGEST =
"00718a8319e67ef6cf9fc389c14ce16784278d55439a7a30a63fa7be8e42bbdf";
@Test
void shouldClearBindingsSessionsAndRestoreSeededRolesAndPasswords() {
@@ -89,7 +90,7 @@ class AuthSecurityResetServiceTest {
Assertions.assertEquals("super-admin-full-01", firstSuperAdmin.getAccountName());
Assertions.assertEquals("超级管理员UKey席位一", firstSuperAdmin.getDisplayName());
Assertions.assertEquals("3df85c2988a5de2b9eeaa8109639641a", firstSuperAdmin.getPasswordSalt());
Assertions.assertEquals(FIRST_SUPER_ADMIN_PASSWORD_HASH, firstSuperAdmin.getPasswordHash());
Assertions.assertEquals(FIRST_SUPER_ADMIN_PASSWORD_DIGEST, firstSuperAdmin.getPasswordDigest());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), firstSuperAdmin.getStatus());
Assertions.assertTrue(Boolean.TRUE.equals(firstSuperAdmin.getNeedChangePassword()));
Assertions.assertEquals(0, firstSuperAdmin.getFailedCount());
@@ -114,7 +115,7 @@ class AuthSecurityResetServiceTest {
entity.setUid(uid);
entity.setAccountName("dirty");
entity.setDisplayName("dirty");
entity.setPasswordHash("dirty");
entity.setPasswordDigest("dirty");
entity.setPasswordSalt("dirty");
entity.setStatus(RoleAccountStatus.LOCKED.name());
entity.setNeedChangePassword(Boolean.FALSE);
@@ -36,6 +36,8 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -99,7 +101,7 @@ class AuthServiceTest {
UkeyLoginRequest ukeyRequest = new UkeyLoginRequest();
ukeyRequest.setRoleCode(RoleCode.KEY_ADMIN.getCode());
ukeyRequest.setLoginFactors(List.of(proofWithPassword("PUB-1", 1, DIGEST_ABC1234, "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1")));
ukeyRequest.setLoginFactors(List.of(proofWithPassword("PUB-1", 1, sm3Hex(DIGEST_ABC1234 + "RB-1"), "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1")));
LoginResponse response = service.ukeyLogin(ukeyRequest);
Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel());
@@ -109,6 +111,34 @@ class AuthServiceTest {
);
}
@Test
void shouldChangePasswordWithBackendIssuedNewSaltAndDynamicOldDigest() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
roles.save(activeRole(RoleCode.KEY_ADMIN));
accounts.save(activeAccount(RoleCode.KEY_ADMIN, 1, "key-admin-01", DIGEST_OLD1234, "SALT-OLD", true));
AuthSessionEntity session = session("token-change-prepare", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name());
session.setAuthenticatedPrincipalsJson("[{\"type\":\"FULL\",\"uid\":1,\"username\":\"key-admin-01\"}]");
sessions.save(session);
AuthService service = newAuthService(roles, accounts, sessions, new InMemoryRoleUkeyBindingRepository(), () -> "unused");
var prepare = service.prepareAccountPasswordChange("token-change-prepare", 1);
service.changeAccountPassword(
"token-change-prepare",
1,
prepare.getChangeToken(),
sm3Hex(DIGEST_OLD1234 + prepare.getOldServerRandom()),
DIGEST_NEW1234
);
AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals("salt-test", changed.getPasswordSalt());
Assertions.assertEquals(DIGEST_NEW1234, changed.getPasswordDigest());
Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword()));
}
@Test
void shouldChangeUnifiedPasswordFromLimitedSessionAndAffectUkeyLogin() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
@@ -124,11 +154,18 @@ class AuthServiceTest {
AuthService service = newAuthService(roles, accounts, sessions, bindings, () -> "token-after-change");
service.changeAccountPassword("token-change-001", 1, DIGEST_OLD1234, DIGEST_NEW1234);
var prepare = service.prepareAccountPasswordChange("token-change-001", 1);
service.changeAccountPassword(
"token-change-001",
1,
prepare.getChangeToken(),
sm3Hex(DIGEST_OLD1234 + prepare.getOldServerRandom()),
DIGEST_NEW1234
);
AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals("salt-test", changed.getPasswordSalt());
Assertions.assertEquals("HASH:" + DIGEST_NEW1234 + ":salt-test", changed.getPasswordHash());
Assertions.assertEquals(DIGEST_NEW1234, changed.getPasswordDigest());
Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword()));
}
@@ -198,8 +235,8 @@ class AuthServiceTest {
UkeyLoginRequest request = new UkeyLoginRequest();
request.setRoleCode(RoleCode.SUPER_ADMIN.getCode());
request.setLoginFactors(List.of(
proofWithPassword("PUB-1-BACKUP", 1, DIGEST_11111111, "RB-1", "ISSUE-1B", "LOGIN-DATA-1B", "LOGIN-SIGN-1B"),
proofWithPassword("PUB-2-BACKUP", 2, DIGEST_22222222, "RB-2", "ISSUE-2B", "LOGIN-DATA-2B", "LOGIN-SIGN-2B")
proofWithPassword("PUB-1-BACKUP", 1, sm3Hex(DIGEST_11111111 + "RB-1"), "RB-1", "ISSUE-1B", "LOGIN-DATA-1B", "LOGIN-SIGN-1B"),
proofWithPassword("PUB-2-BACKUP", 2, sm3Hex(DIGEST_22222222 + "RB-2"), "RB-2", "ISSUE-2B", "LOGIN-DATA-2B", "LOGIN-SIGN-2B")
));
LoginResponse response = service.ukeyLogin(request);
@@ -223,7 +260,7 @@ class AuthServiceTest {
BizException exception = Assertions.assertThrows(BizException.class,
() -> newAuthService(roles, accounts, sessions, new InMemoryRoleUkeyBindingRepository(), () -> "unused")
.changeAccountPassword("token-expired-001", 1, DIGEST_12345678, DIGEST_ABC1234));
.prepareAccountPasswordChange("token-expired-001", 1));
Assertions.assertEquals(ErrorCode.SESSION_INVALID.getCode(), exception.getCode());
Assertions.assertEquals("会话已过期", exception.getMessage());
@@ -328,7 +365,7 @@ class AuthServiceTest {
entity.setAccountName(accountName);
entity.setDisplayName(accountName);
entity.setPasswordSalt(salt);
entity.setPasswordHash("HASH:" + password + ":" + salt);
entity.setPasswordDigest(password);
entity.setStatus(RoleAccountStatus.ACTIVE.name());
entity.setNeedChangePassword(needChangePassword);
entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 1, 0));
@@ -387,15 +424,24 @@ class AuthServiceTest {
return entity;
}
private static String sm3Hex(String value) {
byte[] input = value.getBytes(java.nio.charset.StandardCharsets.UTF_8);
SM3Digest digest = new SM3Digest();
digest.update(input, 0, input.length);
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return Hex.toHexString(result);
}
private static class FakePasswordHasher implements PasswordHasher {
@Override
public String hash(String password, String salt) {
return "HASH:" + password + ":" + salt;
return password;
}
@Override
public boolean matches(String rawPassword, String salt, String expectedHash) {
return hash(rawPassword, salt).equals(expectedHash);
return rawPassword.equals(expectedHash);
}
}