diff --git a/docs/sql/tms_schema_init_steps.md b/docs/sql/tms_schema_init_steps.md index e87fa65..e744fbd 100644 --- a/docs/sql/tms_schema_init_steps.md +++ b/docs/sql/tms_schema_init_steps.md @@ -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 -P -u -p tms < /path/to/V1__tms_schema_full.sql +mysql -h -P -u -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` diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java index 27ec793..fe4acd9 100644 --- a/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java +++ b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java @@ -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 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 prepareAccountPasswordChange( + @PathVariable("uid") Integer uid, + HttpServletRequest httpRequest + ) { + return ApiResponse.success(authService.prepareAccountPasswordChange( + (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN), + uid + )); + } } diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/AdminChangePasswordRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/AdminChangePasswordRequest.java index 928cef3..aa9b096 100644 --- a/src/main/java/com/cisd/tms/modules/auth/dto/AdminChangePasswordRequest.java +++ b/src/main/java/com/cisd/tms/modules/auth/dto/AdminChangePasswordRequest.java @@ -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; } } diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/ChangePasswordRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/ChangePasswordRequest.java index 3e3174f..b230558 100644 --- a/src/main/java/com/cisd/tms/modules/auth/dto/ChangePasswordRequest.java +++ b/src/main/java/com/cisd/tms/modules/auth/dto/ChangePasswordRequest.java @@ -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; } } diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/FullLoginAccountRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/FullLoginAccountRequest.java index 280d5ac..11b2737 100644 --- a/src/main/java/com/cisd/tms/modules/auth/dto/FullLoginAccountRequest.java +++ b/src/main/java/com/cisd/tms/modules/auth/dto/FullLoginAccountRequest.java @@ -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; + } } diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginProof.java b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginProof.java index 053fd44..aa9d0be 100644 --- a/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginProof.java +++ b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginProof.java @@ -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; } } diff --git a/src/main/java/com/cisd/tms/modules/auth/entity/AuthFullAccountEntity.java b/src/main/java/com/cisd/tms/modules/auth/entity/AuthFullAccountEntity.java index 4c6c083..bd8451f 100644 --- a/src/main/java/com/cisd/tms/modules/auth/entity/AuthFullAccountEntity.java +++ b/src/main/java/com/cisd/tms/modules/auth/entity/AuthFullAccountEntity.java @@ -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() { diff --git a/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java b/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java index 8ced75a..f3232ba 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java @@ -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 + ); } diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java index 54586f3..e20f573 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java @@ -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席位要求"); diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthSecurityResetServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthSecurityResetServiceImpl.java index 6f212e7..99aaf0a 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthSecurityResetServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthSecurityResetServiceImpl.java @@ -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 ) { } diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java index 16618e6..a902c00 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java @@ -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 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()) { diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/PasswordDigestSupport.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/PasswordDigestSupport.java index a202c7b..3aef7c4 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/PasswordDigestSupport.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/PasswordDigestSupport.java @@ -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); + } } diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/Pbkdf2PasswordHasher.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/Pbkdf2PasswordHasher.java index 46bf982..0019d38 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/Pbkdf2PasswordHasher.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/Pbkdf2PasswordHasher.java @@ -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); } } diff --git a/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignResult.java b/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignResult.java index 93424db..a555e48 100644 --- a/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignResult.java +++ b/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignResult.java @@ -14,6 +14,11 @@ public class UKeySignResult { */ private String sign; + /** + * 当前角色席位口令盐值。前端写入 UKey,登录时用于计算静态口令摘要。 + */ + private String passwordSalt; + /** * 当发卡流程需要同时下发主密钥备份信息时,统一通过备份包承载。 */ diff --git a/src/main/resources/db/migration/V1__tms_schema_full.sql b/src/main/resources/db/migration/V1__tms_schema_full.sql index 6dc4dcd..aa107ff 100644 --- a/src/main/resources/db/migration/V1__tms_schema_full.sql +++ b/src/main/resources/db/migration/V1__tms_schema_full.sql @@ -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 + 原始口令)'; diff --git a/src/main/resources/db/migration/V2__tms_schema_full.sql b/src/main/resources/db/migration/V2__tms_schema_full.sql index 3c4f115..44657e3 100644 --- a/src/main/resources/db/migration/V2__tms_schema_full.sql +++ b/src/main/resources/db/migration/V2__tms_schema_full.sql @@ -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, diff --git a/src/main/resources/mapper/auth/AuthFullAccountMapper.xml b/src/main/resources/mapper/auth/AuthFullAccountMapper.xml index 5ebb85f..4d9740b 100644 --- a/src/main/resources/mapper/auth/AuthFullAccountMapper.xml +++ b/src/main/resources/mapper/auth/AuthFullAccountMapper.xml @@ -10,7 +10,7 @@ - + @@ -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, diff --git a/src/test/java/com/cisd/tms/TmsApplicationTests.java b/src/test/java/com/cisd/tms/TmsApplicationTests.java index 5d3b6b9..998f429 100644 --- a/src/test/java/com/cisd/tms/TmsApplicationTests.java +++ b/src/test/java/com/cisd/tms/TmsApplicationTests.java @@ -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 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); + } } diff --git a/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java b/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java index e5160d7..c98396a 100644 --- a/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java @@ -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 diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java index e841dba..ee8fbae 100644 --- a/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java @@ -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 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 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 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); } } diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthSecurityResetServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthSecurityResetServiceTest.java index 8187f9e..7b5dc78 100644 --- a/src/test/java/com/cisd/tms/modules/auth/service/AuthSecurityResetServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthSecurityResetServiceTest.java @@ -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); diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java index 45979de..dd4d69a 100644 --- a/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java @@ -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); } }