feat:密码改密复杂度要求和定期30天过期策略
This commit is contained in:
parent
2635f805d1
commit
6c3474e4d6
@ -11,7 +11,7 @@ public class AdminChangePasswordRequest {
|
|||||||
private String oldPassword;
|
private String oldPassword;
|
||||||
|
|
||||||
@NotBlank(message = "newPassword is required")
|
@NotBlank(message = "newPassword is required")
|
||||||
@Schema(description = "新口令", example = "87654321")
|
@Schema(description = "新口令", example = "中Abc1234")
|
||||||
private String newPassword;
|
private String newPassword;
|
||||||
|
|
||||||
public String getOldPassword() {
|
public String getOldPassword() {
|
||||||
|
|||||||
@ -13,7 +13,7 @@ public class ChangePasswordRequest {
|
|||||||
private String oldPassword;
|
private String oldPassword;
|
||||||
|
|
||||||
@NotBlank(message = "newPassword is required")
|
@NotBlank(message = "newPassword is required")
|
||||||
@Schema(description = "新口令", example = "87654321")
|
@Schema(description = "新口令", example = "中Abc1234")
|
||||||
private String newPassword;
|
private String newPassword;
|
||||||
|
|
||||||
public String getOldPassword() {
|
public String getOldPassword() {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ public class AuthFullAccountEntity extends BaseEntity {
|
|||||||
private String passwordSalt;
|
private String passwordSalt;
|
||||||
private String status;
|
private String status;
|
||||||
private Boolean needChangePassword;
|
private Boolean needChangePassword;
|
||||||
|
private LocalDateTime passwordChangedAt;
|
||||||
private Integer failedCount;
|
private Integer failedCount;
|
||||||
private LocalDateTime lockedUntil;
|
private LocalDateTime lockedUntil;
|
||||||
private LocalDateTime lastLoginAt;
|
private LocalDateTime lastLoginAt;
|
||||||
@ -84,6 +85,14 @@ public class AuthFullAccountEntity extends BaseEntity {
|
|||||||
this.needChangePassword = needChangePassword;
|
this.needChangePassword = needChangePassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getPasswordChangedAt() {
|
||||||
|
return passwordChangedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPasswordChangedAt(LocalDateTime passwordChangedAt) {
|
||||||
|
this.passwordChangedAt = passwordChangedAt;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getFailedCount() {
|
public Integer getFailedCount() {
|
||||||
return failedCount;
|
return failedCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ public class AuthUserAccountEntity extends BaseEntity {
|
|||||||
private String passwordSalt;
|
private String passwordSalt;
|
||||||
private String status;
|
private String status;
|
||||||
private Boolean needChangePassword;
|
private Boolean needChangePassword;
|
||||||
|
private LocalDateTime passwordChangedAt;
|
||||||
private Integer failedCount;
|
private Integer failedCount;
|
||||||
private LocalDateTime lockedUntil;
|
private LocalDateTime lockedUntil;
|
||||||
private LocalDateTime lastLoginAt;
|
private LocalDateTime lastLoginAt;
|
||||||
@ -75,6 +76,14 @@ public class AuthUserAccountEntity extends BaseEntity {
|
|||||||
this.needChangePassword = needChangePassword;
|
this.needChangePassword = needChangePassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getPasswordChangedAt() {
|
||||||
|
return passwordChangedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPasswordChangedAt(LocalDateTime passwordChangedAt) {
|
||||||
|
this.passwordChangedAt = passwordChangedAt;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getFailedCount() {
|
public Integer getFailedCount() {
|
||||||
return failedCount;
|
return failedCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
package com.cisd.tms.modules.auth.service;
|
||||||
|
|
||||||
|
import com.cisd.tms.common.enums.ErrorCode;
|
||||||
|
import com.cisd.tms.common.exception.BizException;
|
||||||
|
|
||||||
|
public final class PasswordComplexityValidator {
|
||||||
|
|
||||||
|
public static final String MESSAGE = "密码复杂度不符合要求:需包含中文、英文、数字或特殊字符,长度不少于八位";
|
||||||
|
|
||||||
|
private PasswordComplexityValidator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void validate(String password) {
|
||||||
|
String value = password == null ? "" : password;
|
||||||
|
if (value.length() < 8 || !containsChinese(value) || !containsEnglish(value) || !containsDigitOrSpecial(value)) {
|
||||||
|
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean containsChinese(String value) {
|
||||||
|
return value.codePoints().anyMatch(codePoint -> Character.UnicodeScript.of(codePoint) == Character.UnicodeScript.HAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean containsEnglish(String value) {
|
||||||
|
return value.codePoints().anyMatch(codePoint ->
|
||||||
|
(codePoint >= 'A' && codePoint <= 'Z') || (codePoint >= 'a' && codePoint <= 'z'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean containsDigitOrSpecial(String value) {
|
||||||
|
return value.codePoints().anyMatch(codePoint ->
|
||||||
|
Character.isDigit(codePoint) || isSpecial(codePoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSpecial(int codePoint) {
|
||||||
|
return !Character.isWhitespace(codePoint)
|
||||||
|
&& !Character.isDigit(codePoint)
|
||||||
|
&& !isChineseOrEnglish(codePoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isChineseOrEnglish(int codePoint) {
|
||||||
|
return Character.UnicodeScript.of(codePoint) == Character.UnicodeScript.HAN
|
||||||
|
|| (codePoint >= 'A' && codePoint <= 'Z')
|
||||||
|
|| (codePoint >= 'a' && codePoint <= 'z');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ import com.cisd.tms.modules.auth.repository.AuthUserAccountRepository;
|
|||||||
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
|
||||||
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
|
||||||
import com.cisd.tms.modules.auth.service.AuthAdminService;
|
import com.cisd.tms.modules.auth.service.AuthAdminService;
|
||||||
|
import com.cisd.tms.modules.auth.service.PasswordComplexityValidator;
|
||||||
import com.cisd.tms.modules.auth.service.PasswordHasher;
|
import com.cisd.tms.modules.auth.service.PasswordHasher;
|
||||||
import com.cisd.tms.modules.auth.service.PasswordSaltGenerator;
|
import com.cisd.tms.modules.auth.service.PasswordSaltGenerator;
|
||||||
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
|
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
|
||||||
@ -87,6 +88,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
|||||||
RoleAccountEntity target = loadRole(targetRoleCode);
|
RoleAccountEntity target = loadRole(targetRoleCode);
|
||||||
target.setStatus(RoleAccountStatus.ACTIVE.name());
|
target.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
roleAccountRepository.update(target);
|
roleAccountRepository.update(target);
|
||||||
|
LocalDateTime current = now();
|
||||||
|
|
||||||
List<AuthFullAccountEntity> fullAccounts = authFullAccountRepository.findByRoleCode(targetRoleCode);
|
List<AuthFullAccountEntity> fullAccounts = authFullAccountRepository.findByRoleCode(targetRoleCode);
|
||||||
if (fullAccounts.isEmpty()) {
|
if (fullAccounts.isEmpty()) {
|
||||||
@ -98,6 +100,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
|||||||
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
|
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
|
||||||
account.setStatus(RoleAccountStatus.ACTIVE.name());
|
account.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
account.setNeedChangePassword(Boolean.TRUE);
|
account.setNeedChangePassword(Boolean.TRUE);
|
||||||
|
account.setPasswordChangedAt(current);
|
||||||
account.setFailedCount(0);
|
account.setFailedCount(0);
|
||||||
account.setLockedUntil(null);
|
account.setLockedUntil(null);
|
||||||
account.setLastActiveAt(null);
|
account.setLastActiveAt(null);
|
||||||
@ -115,6 +118,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
|||||||
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
|
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
|
||||||
account.setStatus(RoleAccountStatus.ACTIVE.name());
|
account.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
account.setNeedChangePassword(Boolean.TRUE);
|
account.setNeedChangePassword(Boolean.TRUE);
|
||||||
|
account.setPasswordChangedAt(current);
|
||||||
account.setFailedCount(0);
|
account.setFailedCount(0);
|
||||||
account.setLockedUntil(null);
|
account.setLockedUntil(null);
|
||||||
account.setLastActiveAt(null);
|
account.setLastActiveAt(null);
|
||||||
@ -132,6 +136,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
|||||||
String oldPassword,
|
String oldPassword,
|
||||||
String newPassword
|
String newPassword
|
||||||
) {
|
) {
|
||||||
|
PasswordComplexityValidator.validate(newPassword);
|
||||||
loadRole(targetRoleCode);
|
loadRole(targetRoleCode);
|
||||||
RoleCode targetRole = resolveRoleCode(targetRoleCode);
|
RoleCode targetRole = resolveRoleCode(targetRoleCode);
|
||||||
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
|
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
|
||||||
@ -155,6 +160,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
|||||||
String oldPassword,
|
String oldPassword,
|
||||||
String newPassword
|
String newPassword
|
||||||
) {
|
) {
|
||||||
|
PasswordComplexityValidator.validate(newPassword);
|
||||||
loadRole(targetRoleCode);
|
loadRole(targetRoleCode);
|
||||||
AuthUserAccountEntity account = authUserAccountRepository.findByUsername(username)
|
AuthUserAccountEntity account = authUserAccountRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user account not found"));
|
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role user account not found"));
|
||||||
@ -262,6 +268,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
|||||||
account.setPasswordSalt(newSalt);
|
account.setPasswordSalt(newSalt);
|
||||||
account.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
|
account.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
|
||||||
account.setNeedChangePassword(Boolean.TRUE);
|
account.setNeedChangePassword(Boolean.TRUE);
|
||||||
|
account.setPasswordChangedAt(now());
|
||||||
account.setFailedCount(0);
|
account.setFailedCount(0);
|
||||||
account.setLockedUntil(null);
|
account.setLockedUntil(null);
|
||||||
if (RoleAccountStatus.LOCKED.name().equals(account.getStatus())) {
|
if (RoleAccountStatus.LOCKED.name().equals(account.getStatus())) {
|
||||||
@ -274,6 +281,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
|||||||
account.setPasswordSalt(newSalt);
|
account.setPasswordSalt(newSalt);
|
||||||
account.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
|
account.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
|
||||||
account.setNeedChangePassword(Boolean.TRUE);
|
account.setNeedChangePassword(Boolean.TRUE);
|
||||||
|
account.setPasswordChangedAt(now());
|
||||||
account.setFailedCount(0);
|
account.setFailedCount(0);
|
||||||
account.setLockedUntil(null);
|
account.setLockedUntil(null);
|
||||||
if (RoleAccountStatus.LOCKED.name().equals(account.getStatus())) {
|
if (RoleAccountStatus.LOCKED.name().equals(account.getStatus())) {
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import com.cisd.tms.modules.auth.service.AuthService;
|
|||||||
import com.cisd.tms.modules.auth.service.CaptchaService;
|
import com.cisd.tms.modules.auth.service.CaptchaService;
|
||||||
import com.cisd.tms.modules.auth.service.CompatUkeyVerifier;
|
import com.cisd.tms.modules.auth.service.CompatUkeyVerifier;
|
||||||
import com.cisd.tms.modules.auth.service.PasswordHasher;
|
import com.cisd.tms.modules.auth.service.PasswordHasher;
|
||||||
|
import com.cisd.tms.modules.auth.service.PasswordComplexityValidator;
|
||||||
import com.cisd.tms.modules.auth.service.PasswordSaltGenerator;
|
import com.cisd.tms.modules.auth.service.PasswordSaltGenerator;
|
||||||
import com.cisd.tms.modules.auth.service.SessionTokenGenerator;
|
import com.cisd.tms.modules.auth.service.SessionTokenGenerator;
|
||||||
import com.cisd.tms.modules.auth.service.UkeyLoginRandomService;
|
import com.cisd.tms.modules.auth.service.UkeyLoginRandomService;
|
||||||
@ -65,6 +66,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
|
|
||||||
private static final int MAX_FAILED_ATTEMPTS = 5;
|
private static final int MAX_FAILED_ATTEMPTS = 5;
|
||||||
private static final int IDLE_TIMEOUT_MINUTES = 10;
|
private static final int IDLE_TIMEOUT_MINUTES = 10;
|
||||||
|
private static final int PASSWORD_EXPIRE_DAYS = 30;
|
||||||
|
|
||||||
private final RoleAccountRepository roleAccountRepository;
|
private final RoleAccountRepository roleAccountRepository;
|
||||||
private final AuthUserRepository authUserRepository;
|
private final AuthUserRepository authUserRepository;
|
||||||
@ -236,6 +238,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void changeFullAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword) {
|
public void changeFullAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword) {
|
||||||
|
PasswordComplexityValidator.validate(newPassword);
|
||||||
AuthSessionEntity session = requireActiveSession(sessionToken);
|
AuthSessionEntity session = requireActiveSession(sessionToken);
|
||||||
if (!AuthLevel.FULL.name().equals(session.getAuthLevel())) {
|
if (!AuthLevel.FULL.name().equals(session.getAuthLevel())) {
|
||||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full session is required");
|
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full session is required");
|
||||||
@ -258,6 +261,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
fullAccount.setLockedUntil(null);
|
fullAccount.setLockedUntil(null);
|
||||||
fullAccount.setStatus(RoleAccountStatus.ACTIVE.name());
|
fullAccount.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
fullAccount.setNeedChangePassword(Boolean.FALSE);
|
fullAccount.setNeedChangePassword(Boolean.FALSE);
|
||||||
|
fullAccount.setPasswordChangedAt(current);
|
||||||
fullAccount.setLastLoginAt(current);
|
fullAccount.setLastLoginAt(current);
|
||||||
fullAccount.setLastActiveAt(current);
|
fullAccount.setLastActiveAt(current);
|
||||||
authFullAccountRepository.update(fullAccount);
|
authFullAccountRepository.update(fullAccount);
|
||||||
@ -269,6 +273,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void changeLimitedAccountPassword(String sessionToken, String username, String currentPassword, String newPassword) {
|
public void changeLimitedAccountPassword(String sessionToken, String username, String currentPassword, String newPassword) {
|
||||||
|
PasswordComplexityValidator.validate(newPassword);
|
||||||
AuthSessionEntity session = requireActiveSession(sessionToken);
|
AuthSessionEntity session = requireActiveSession(sessionToken);
|
||||||
if (!AuthLevel.LIMITED.name().equals(session.getAuthLevel())) {
|
if (!AuthLevel.LIMITED.name().equals(session.getAuthLevel())) {
|
||||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "limited session is required");
|
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "limited session is required");
|
||||||
@ -292,6 +297,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
userAccount.setLockedUntil(null);
|
userAccount.setLockedUntil(null);
|
||||||
userAccount.setStatus(RoleAccountStatus.ACTIVE.name());
|
userAccount.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
userAccount.setNeedChangePassword(Boolean.FALSE);
|
userAccount.setNeedChangePassword(Boolean.FALSE);
|
||||||
|
userAccount.setPasswordChangedAt(current);
|
||||||
userAccount.setLastLoginAt(current);
|
userAccount.setLastLoginAt(current);
|
||||||
userAccount.setLastActiveAt(current);
|
userAccount.setLastActiveAt(current);
|
||||||
authUserAccountRepository.update(userAccount);
|
authUserAccountRepository.update(userAccount);
|
||||||
@ -652,11 +658,11 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean needsPasswordChangeForLimitedAccounts(List<AuthUserAccountEntity> accounts) {
|
private boolean needsPasswordChangeForLimitedAccounts(List<AuthUserAccountEntity> accounts) {
|
||||||
return accounts != null && accounts.stream().anyMatch(account -> Boolean.TRUE.equals(account.getNeedChangePassword()));
|
return accounts != null && accounts.stream().anyMatch(this::needsPasswordChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean needsPasswordChangeForFullAccounts(List<AuthFullAccountEntity> accounts) {
|
private boolean needsPasswordChangeForFullAccounts(List<AuthFullAccountEntity> accounts) {
|
||||||
return accounts != null && accounts.stream().anyMatch(account -> Boolean.TRUE.equals(account.getNeedChangePassword()));
|
return accounts != null && accounts.stream().anyMatch(this::needsPasswordChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean needsPasswordChange(AuthSessionEntity session) {
|
private boolean needsPasswordChange(AuthSessionEntity session) {
|
||||||
@ -672,14 +678,28 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
return accounts.stream()
|
return accounts.stream()
|
||||||
.filter(account -> principals.stream()
|
.filter(account -> principals.stream()
|
||||||
.anyMatch(principal -> "FULL".equals(principal.getType()) && account.getUid().equals(principal.getUid())))
|
.anyMatch(principal -> "FULL".equals(principal.getType()) && account.getUid().equals(principal.getUid())))
|
||||||
.anyMatch(account -> Boolean.TRUE.equals(account.getNeedChangePassword()));
|
.anyMatch(this::needsPasswordChange);
|
||||||
}
|
}
|
||||||
List<AuthUserAccountEntity> accounts = authUserAccountRepository.findByRoleCode(session.getRoleCode());
|
List<AuthUserAccountEntity> accounts = authUserAccountRepository.findByRoleCode(session.getRoleCode());
|
||||||
return accounts.stream()
|
return accounts.stream()
|
||||||
.filter(account -> principals.stream()
|
.filter(account -> principals.stream()
|
||||||
.anyMatch(principal -> "LIMITED".equals(principal.getType())
|
.anyMatch(principal -> "LIMITED".equals(principal.getType())
|
||||||
&& account.getUsername().equals(principal.getUsername())))
|
&& account.getUsername().equals(principal.getUsername())))
|
||||||
.anyMatch(account -> Boolean.TRUE.equals(account.getNeedChangePassword()));
|
.anyMatch(this::needsPasswordChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean needsPasswordChange(AuthFullAccountEntity account) {
|
||||||
|
return Boolean.TRUE.equals(account.getNeedChangePassword())
|
||||||
|
|| isPasswordExpired(account.getPasswordChangedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean needsPasswordChange(AuthUserAccountEntity account) {
|
||||||
|
return Boolean.TRUE.equals(account.getNeedChangePassword())
|
||||||
|
|| isPasswordExpired(account.getPasswordChangedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPasswordExpired(LocalDateTime passwordChangedAt) {
|
||||||
|
return passwordChangedAt == null || !passwordChangedAt.isAfter(now().minusDays(PASSWORD_EXPIRE_DAYS));
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthSessionEntity requireActiveSession(String sessionToken) {
|
private AuthSessionEntity requireActiveSession(String sessionToken) {
|
||||||
|
|||||||
@ -275,6 +275,7 @@ CREATE TABLE IF NOT EXISTS tms_auth_user_account (
|
|||||||
password_salt VARCHAR(128) NOT NULL,
|
password_salt VARCHAR(128) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
need_change_password TINYINT(1) NOT NULL DEFAULT 1,
|
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,
|
failed_count INT NOT NULL DEFAULT 0,
|
||||||
locked_until DATETIME(3) NULL,
|
locked_until DATETIME(3) NULL,
|
||||||
last_login_at DATETIME(3) NULL,
|
last_login_at DATETIME(3) NULL,
|
||||||
@ -295,6 +296,7 @@ CREATE TABLE IF NOT EXISTS tms_auth_full_account (
|
|||||||
password_salt VARCHAR(128) NOT NULL,
|
password_salt VARCHAR(128) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
need_change_password TINYINT(1) NOT NULL DEFAULT 1,
|
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,
|
failed_count INT NOT NULL DEFAULT 0,
|
||||||
locked_until DATETIME(3) NULL,
|
locked_until DATETIME(3) NULL,
|
||||||
last_login_at DATETIME(3) NULL,
|
last_login_at DATETIME(3) NULL,
|
||||||
@ -445,6 +447,7 @@ INSERT IGNORE INTO tms_auth_user_account (
|
|||||||
password_salt,
|
password_salt,
|
||||||
status,
|
status,
|
||||||
need_change_password,
|
need_change_password,
|
||||||
|
password_changed_at,
|
||||||
failed_count,
|
failed_count,
|
||||||
locked_until,
|
locked_until,
|
||||||
last_login_at,
|
last_login_at,
|
||||||
@ -462,6 +465,7 @@ VALUES
|
|||||||
'init-super-admin-salt-20260325',
|
'init-super-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
1,
|
1,
|
||||||
|
CURRENT_TIMESTAMP(3),
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -478,6 +482,7 @@ VALUES
|
|||||||
'init-super-admin-salt-20260325',
|
'init-super-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
1,
|
1,
|
||||||
|
CURRENT_TIMESTAMP(3),
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -494,6 +499,7 @@ VALUES
|
|||||||
'init-key-admin-salt-20260325',
|
'init-key-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
1,
|
1,
|
||||||
|
CURRENT_TIMESTAMP(3),
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -510,6 +516,7 @@ VALUES
|
|||||||
'init-audit-admin-salt-20260325',
|
'init-audit-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
1,
|
1,
|
||||||
|
CURRENT_TIMESTAMP(3),
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -526,6 +533,7 @@ VALUES
|
|||||||
'init-ops-admin-salt-20260325',
|
'init-ops-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
1,
|
1,
|
||||||
|
CURRENT_TIMESTAMP(3),
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -544,6 +552,7 @@ INSERT IGNORE INTO tms_auth_full_account (
|
|||||||
password_salt,
|
password_salt,
|
||||||
status,
|
status,
|
||||||
need_change_password,
|
need_change_password,
|
||||||
|
password_changed_at,
|
||||||
failed_count,
|
failed_count,
|
||||||
locked_until,
|
locked_until,
|
||||||
last_login_at,
|
last_login_at,
|
||||||
@ -562,6 +571,7 @@ VALUES
|
|||||||
'init-super-admin-salt-20260325',
|
'init-super-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
1,
|
1,
|
||||||
|
CURRENT_TIMESTAMP(3),
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -579,6 +589,7 @@ VALUES
|
|||||||
'init-super-admin-salt-20260325',
|
'init-super-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
1,
|
1,
|
||||||
|
CURRENT_TIMESTAMP(3),
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -596,6 +607,7 @@ VALUES
|
|||||||
'init-key-admin-salt-20260325',
|
'init-key-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
1,
|
1,
|
||||||
|
CURRENT_TIMESTAMP(3),
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -613,6 +625,7 @@ VALUES
|
|||||||
'init-audit-admin-salt-20260325',
|
'init-audit-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
1,
|
1,
|
||||||
|
CURRENT_TIMESTAMP(3),
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
@ -630,6 +643,7 @@ VALUES
|
|||||||
'init-ops-admin-salt-20260325',
|
'init-ops-admin-salt-20260325',
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
1,
|
1,
|
||||||
|
CURRENT_TIMESTAMP(3),
|
||||||
0,
|
0,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
<result property="passwordSalt" column="password_salt"/>
|
<result property="passwordSalt" column="password_salt"/>
|
||||||
<result property="status" column="status"/>
|
<result property="status" column="status"/>
|
||||||
<result property="needChangePassword" column="need_change_password"/>
|
<result property="needChangePassword" column="need_change_password"/>
|
||||||
|
<result property="passwordChangedAt" column="password_changed_at"/>
|
||||||
<result property="failedCount" column="failed_count"/>
|
<result property="failedCount" column="failed_count"/>
|
||||||
<result property="lockedUntil" column="locked_until"/>
|
<result property="lockedUntil" column="locked_until"/>
|
||||||
<result property="lastLoginAt" column="last_login_at"/>
|
<result property="lastLoginAt" column="last_login_at"/>
|
||||||
@ -32,6 +33,7 @@
|
|||||||
password_salt,
|
password_salt,
|
||||||
status,
|
status,
|
||||||
need_change_password,
|
need_change_password,
|
||||||
|
password_changed_at,
|
||||||
failed_count,
|
failed_count,
|
||||||
locked_until,
|
locked_until,
|
||||||
last_login_at,
|
last_login_at,
|
||||||
@ -54,6 +56,7 @@
|
|||||||
password_salt,
|
password_salt,
|
||||||
status,
|
status,
|
||||||
need_change_password,
|
need_change_password,
|
||||||
|
password_changed_at,
|
||||||
failed_count,
|
failed_count,
|
||||||
locked_until,
|
locked_until,
|
||||||
last_login_at,
|
last_login_at,
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
<result property="passwordSalt" column="password_salt"/>
|
<result property="passwordSalt" column="password_salt"/>
|
||||||
<result property="status" column="status"/>
|
<result property="status" column="status"/>
|
||||||
<result property="needChangePassword" column="need_change_password"/>
|
<result property="needChangePassword" column="need_change_password"/>
|
||||||
|
<result property="passwordChangedAt" column="password_changed_at"/>
|
||||||
<result property="failedCount" column="failed_count"/>
|
<result property="failedCount" column="failed_count"/>
|
||||||
<result property="lockedUntil" column="locked_until"/>
|
<result property="lockedUntil" column="locked_until"/>
|
||||||
<result property="lastLoginAt" column="last_login_at"/>
|
<result property="lastLoginAt" column="last_login_at"/>
|
||||||
@ -30,6 +31,7 @@
|
|||||||
password_salt,
|
password_salt,
|
||||||
status,
|
status,
|
||||||
need_change_password,
|
need_change_password,
|
||||||
|
password_changed_at,
|
||||||
failed_count,
|
failed_count,
|
||||||
locked_until,
|
locked_until,
|
||||||
last_login_at,
|
last_login_at,
|
||||||
@ -50,6 +52,7 @@
|
|||||||
password_salt,
|
password_salt,
|
||||||
status,
|
status,
|
||||||
need_change_password,
|
need_change_password,
|
||||||
|
password_changed_at,
|
||||||
failed_count,
|
failed_count,
|
||||||
locked_until,
|
locked_until,
|
||||||
last_login_at,
|
last_login_at,
|
||||||
|
|||||||
@ -214,13 +214,13 @@ class AuthControllerTest {
|
|||||||
.content("""
|
.content("""
|
||||||
{
|
{
|
||||||
"oldPassword": "12345678",
|
"oldPassword": "12345678",
|
||||||
"newPassword": "87654321"
|
"newPassword": "中Abc1234"
|
||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().string(containsString("\"success\":true")));
|
.andExpect(content().string(containsString("\"success\":true")));
|
||||||
|
|
||||||
Mockito.verify(authService).changeFullAccountPassword("token-change-001", 1, "12345678", "87654321");
|
Mockito.verify(authService).changeFullAccountPassword("token-change-001", 1, "12345678", "中Abc1234");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -239,13 +239,13 @@ class AuthControllerTest {
|
|||||||
.content("""
|
.content("""
|
||||||
{
|
{
|
||||||
"oldPassword": "12345678",
|
"oldPassword": "12345678",
|
||||||
"newPassword": "87654321"
|
"newPassword": "中Abc1234"
|
||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().string(containsString("\"success\":true")));
|
.andExpect(content().string(containsString("\"success\":true")));
|
||||||
|
|
||||||
Mockito.verify(authService).changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "87654321");
|
Mockito.verify(authService).changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "中Abc1234");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -312,13 +312,13 @@ class AuthControllerTest {
|
|||||||
.content("""
|
.content("""
|
||||||
{
|
{
|
||||||
"oldPassword": "12345678",
|
"oldPassword": "12345678",
|
||||||
"newPassword": "87654321"
|
"newPassword": "中Abc1234"
|
||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().string(containsString("\"success\":true")));
|
.andExpect(content().string(containsString("\"success\":true")));
|
||||||
|
|
||||||
Mockito.verify(authAdminService).changeFullAccountPassword("SUPER_ADMIN", "FULL", "KEY_ADMIN", 1, "12345678", "87654321");
|
Mockito.verify(authAdminService).changeFullAccountPassword("SUPER_ADMIN", "FULL", "KEY_ADMIN", 1, "12345678", "中Abc1234");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -338,13 +338,13 @@ class AuthControllerTest {
|
|||||||
.content("""
|
.content("""
|
||||||
{
|
{
|
||||||
"oldPassword": "12345678",
|
"oldPassword": "12345678",
|
||||||
"newPassword": "87654321"
|
"newPassword": "中Abc1234"
|
||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().string(containsString("\"success\":true")));
|
.andExpect(content().string(containsString("\"success\":true")));
|
||||||
|
|
||||||
Mockito.verify(authAdminService).changeLimitedAccountPassword("SUPER_ADMIN", "FULL", "AUDIT_ADMIN", "audit-admin-01", "12345678", "87654321");
|
Mockito.verify(authAdminService).changeLimitedAccountPassword("SUPER_ADMIN", "FULL", "AUDIT_ADMIN", "audit-admin-01", "12345678", "中Abc1234");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -107,6 +107,8 @@ class AuthAdminServiceTest {
|
|||||||
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), fullSecond.getStatus());
|
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), fullSecond.getStatus());
|
||||||
Assertions.assertTrue(Boolean.TRUE.equals(fullFirst.getNeedChangePassword()));
|
Assertions.assertTrue(Boolean.TRUE.equals(fullFirst.getNeedChangePassword()));
|
||||||
Assertions.assertTrue(Boolean.TRUE.equals(fullSecond.getNeedChangePassword()));
|
Assertions.assertTrue(Boolean.TRUE.equals(fullSecond.getNeedChangePassword()));
|
||||||
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), fullFirst.getPasswordChangedAt());
|
||||||
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), fullSecond.getPasswordChangedAt());
|
||||||
Assertions.assertEquals(0, fullFirst.getFailedCount());
|
Assertions.assertEquals(0, fullFirst.getFailedCount());
|
||||||
Assertions.assertEquals(0, fullSecond.getFailedCount());
|
Assertions.assertEquals(0, fullSecond.getFailedCount());
|
||||||
Assertions.assertNull(fullFirst.getLockedUntil());
|
Assertions.assertNull(fullFirst.getLockedUntil());
|
||||||
@ -119,6 +121,8 @@ class AuthAdminServiceTest {
|
|||||||
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), second.getStatus());
|
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), second.getStatus());
|
||||||
Assertions.assertTrue(Boolean.TRUE.equals(first.getNeedChangePassword()));
|
Assertions.assertTrue(Boolean.TRUE.equals(first.getNeedChangePassword()));
|
||||||
Assertions.assertTrue(Boolean.TRUE.equals(second.getNeedChangePassword()));
|
Assertions.assertTrue(Boolean.TRUE.equals(second.getNeedChangePassword()));
|
||||||
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), first.getPasswordChangedAt());
|
||||||
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), second.getPasswordChangedAt());
|
||||||
Assertions.assertEquals(0, first.getFailedCount());
|
Assertions.assertEquals(0, first.getFailedCount());
|
||||||
Assertions.assertEquals(0, second.getFailedCount());
|
Assertions.assertEquals(0, second.getFailedCount());
|
||||||
Assertions.assertNull(first.getLockedUntil());
|
Assertions.assertNull(first.getLockedUntil());
|
||||||
@ -155,14 +159,15 @@ class AuthAdminServiceTest {
|
|||||||
RoleCode.KEY_ADMIN.getCode(),
|
RoleCode.KEY_ADMIN.getCode(),
|
||||||
1,
|
1,
|
||||||
"12345678",
|
"12345678",
|
||||||
"87654321"
|
"中Abc1234"
|
||||||
);
|
);
|
||||||
|
|
||||||
AuthFullAccountEntity changed = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
|
AuthFullAccountEntity changed = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
|
||||||
Assertions.assertEquals("salt-full-admin", changed.getPasswordSalt());
|
Assertions.assertEquals("salt-full-admin", changed.getPasswordSalt());
|
||||||
Assertions.assertEquals("HASH:87654321:salt-full-admin", changed.getPasswordHash());
|
Assertions.assertEquals("HASH:中Abc1234:salt-full-admin", changed.getPasswordHash());
|
||||||
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus());
|
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus());
|
||||||
Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword()));
|
Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword()));
|
||||||
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), changed.getPasswordChangedAt());
|
||||||
Assertions.assertEquals(0, changed.getFailedCount());
|
Assertions.assertEquals(0, changed.getFailedCount());
|
||||||
Assertions.assertNull(changed.getLockedUntil());
|
Assertions.assertNull(changed.getLockedUntil());
|
||||||
}
|
}
|
||||||
@ -193,14 +198,15 @@ class AuthAdminServiceTest {
|
|||||||
RoleCode.AUDIT_ADMIN.getCode(),
|
RoleCode.AUDIT_ADMIN.getCode(),
|
||||||
"audit-admin-01",
|
"audit-admin-01",
|
||||||
"12345678",
|
"12345678",
|
||||||
"87654321"
|
"中Abc1234"
|
||||||
);
|
);
|
||||||
|
|
||||||
AuthUserAccountEntity changed = userAccounts.findByUsername("audit-admin-01").orElseThrow();
|
AuthUserAccountEntity changed = userAccounts.findByUsername("audit-admin-01").orElseThrow();
|
||||||
Assertions.assertEquals("salt-limited-admin", changed.getPasswordSalt());
|
Assertions.assertEquals("salt-limited-admin", changed.getPasswordSalt());
|
||||||
Assertions.assertEquals("HASH:87654321:salt-limited-admin", changed.getPasswordHash());
|
Assertions.assertEquals("HASH:中Abc1234:salt-limited-admin", changed.getPasswordHash());
|
||||||
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus());
|
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus());
|
||||||
Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword()));
|
Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword()));
|
||||||
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), changed.getPasswordChangedAt());
|
||||||
Assertions.assertEquals(0, changed.getFailedCount());
|
Assertions.assertEquals(0, changed.getFailedCount());
|
||||||
Assertions.assertNull(changed.getLockedUntil());
|
Assertions.assertNull(changed.getLockedUntil());
|
||||||
}
|
}
|
||||||
@ -229,7 +235,7 @@ class AuthAdminServiceTest {
|
|||||||
RoleCode.AUDIT_ADMIN.getCode(),
|
RoleCode.AUDIT_ADMIN.getCode(),
|
||||||
"audit-admin-01",
|
"audit-admin-01",
|
||||||
"bad-password",
|
"bad-password",
|
||||||
"87654321"
|
"中Abc1234"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -239,6 +245,37 @@ class AuthAdminServiceTest {
|
|||||||
Assertions.assertEquals("HASH:12345678:OLD-SALT", unchanged.getPasswordHash());
|
Assertions.assertEquals("HASH:12345678:OLD-SALT", unchanged.getPasswordHash());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectAdminPasswordChangeWhenNewPasswordIsTooSimple() {
|
||||||
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
|
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
||||||
|
roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.ACTIVE));
|
||||||
|
userAccounts.save(user("audit-admin-01", RoleCode.AUDIT_ADMIN, "HASH:12345678:OLD-SALT", "OLD-SALT"));
|
||||||
|
|
||||||
|
AuthAdminService service = newAuthAdminService(
|
||||||
|
roleAccounts,
|
||||||
|
new InMemoryAuthFullAccountRepository(),
|
||||||
|
userAccounts,
|
||||||
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
|
FIXED_CLOCK,
|
||||||
|
new FixedSaltSupplier("salt-unused")
|
||||||
|
);
|
||||||
|
|
||||||
|
com.cisd.tms.common.exception.BizException exception = Assertions.assertThrows(
|
||||||
|
com.cisd.tms.common.exception.BizException.class,
|
||||||
|
() -> service.changeLimitedAccountPassword(
|
||||||
|
RoleCode.SUPER_ADMIN.getCode(),
|
||||||
|
AuthLevel.FULL.name(),
|
||||||
|
RoleCode.AUDIT_ADMIN.getCode(),
|
||||||
|
"audit-admin-01",
|
||||||
|
"12345678",
|
||||||
|
"87654321"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Assertions.assertEquals("密码复杂度不符合要求:需包含中文、英文、数字或特殊字符,长度不少于八位", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReplaceActiveBindingInSameUidSeat() {
|
void shouldReplaceActiveBindingInSameUidSeat() {
|
||||||
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
|
InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository();
|
||||||
@ -324,6 +361,7 @@ class AuthAdminServiceTest {
|
|||||||
entity.setPasswordSalt(passwordSalt);
|
entity.setPasswordSalt(passwordSalt);
|
||||||
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
entity.setNeedChangePassword(Boolean.FALSE);
|
entity.setNeedChangePassword(Boolean.FALSE);
|
||||||
|
entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 2, 0));
|
||||||
entity.setFailedCount(0);
|
entity.setFailedCount(0);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
@ -339,6 +377,7 @@ class AuthAdminServiceTest {
|
|||||||
entity.setPasswordSalt(passwordSalt);
|
entity.setPasswordSalt(passwordSalt);
|
||||||
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
entity.setNeedChangePassword(Boolean.FALSE);
|
entity.setNeedChangePassword(Boolean.FALSE);
|
||||||
|
entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 2, 0));
|
||||||
entity.setFailedCount(0);
|
entity.setFailedCount(0);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,6 +83,68 @@ class AuthServiceTest {
|
|||||||
Assertions.assertEquals("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"audit-admin-01\"}]", session.getAuthenticatedPrincipalsJson());
|
Assertions.assertEquals("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"audit-admin-01\"}]", session.getAuthenticatedPrincipalsJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRequirePasswordChangeWhenLimitedAccountPasswordIsExpired() {
|
||||||
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
|
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
||||||
|
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN));
|
||||||
|
AuthUserAccountEntity account = activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null);
|
||||||
|
account.setPasswordChangedAt(LocalDateTime.of(2026, 2, 21, 2, 0));
|
||||||
|
userAccounts.save(account);
|
||||||
|
|
||||||
|
AuthService service = newAuthService(
|
||||||
|
roleAccounts,
|
||||||
|
userAccounts,
|
||||||
|
new InMemoryAuthFullAccountRepository(),
|
||||||
|
new InMemoryAuthSessionRepository(),
|
||||||
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
|
FIXED_CLOCK,
|
||||||
|
() -> "token-limited-expired-001"
|
||||||
|
);
|
||||||
|
|
||||||
|
LoginRequest request = new LoginRequest();
|
||||||
|
request.setRoleCode(RoleCode.AUDIT_ADMIN.getCode());
|
||||||
|
request.setAccounts(List.of(account("audit-admin-01", "12345678")));
|
||||||
|
|
||||||
|
LoginResponse response = service.login(request);
|
||||||
|
|
||||||
|
Assertions.assertEquals("token-limited-expired-001", response.getToken());
|
||||||
|
Assertions.assertTrue(response.getNeedChangePassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRequirePasswordChangeWhenFullAccountPasswordIsExpired() {
|
||||||
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
|
InMemoryAuthFullAccountRepository fullAccounts = new InMemoryAuthFullAccountRepository();
|
||||||
|
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
|
||||||
|
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN));
|
||||||
|
AuthFullAccountEntity account = activeFullAccount(RoleCode.KEY_ADMIN, 1, "key-admin-full-01", "12345678", "SALT-K", 0, null);
|
||||||
|
account.setPasswordChangedAt(LocalDateTime.of(2026, 2, 21, 2, 0));
|
||||||
|
fullAccounts.save(account);
|
||||||
|
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1"));
|
||||||
|
|
||||||
|
AuthService service = newAuthService(
|
||||||
|
roleAccounts,
|
||||||
|
new InMemoryAuthUserAccountRepository(),
|
||||||
|
fullAccounts,
|
||||||
|
new InMemoryAuthSessionRepository(),
|
||||||
|
ukeyBindings,
|
||||||
|
FIXED_CLOCK,
|
||||||
|
() -> "token-full-expired-001"
|
||||||
|
);
|
||||||
|
|
||||||
|
LoginRequest request = new LoginRequest();
|
||||||
|
request.setRoleCode(RoleCode.KEY_ADMIN.getCode());
|
||||||
|
request.setUkeySerials(List.of("UK-1"));
|
||||||
|
request.setFullAccounts(List.of(fullAccount(1, "12345678")));
|
||||||
|
|
||||||
|
LoginResponse response = service.login(request);
|
||||||
|
|
||||||
|
Assertions.assertEquals("token-full-expired-001", response.getToken());
|
||||||
|
Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel());
|
||||||
|
Assertions.assertTrue(response.getNeedChangePassword());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRequireTwoAccountsForSuperAdminPasswordLogin() {
|
void shouldRequireTwoAccountsForSuperAdminPasswordLogin() {
|
||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
@ -177,6 +239,35 @@ class AuthServiceTest {
|
|||||||
Assertions.assertTrue(response.getNeedChangePassword());
|
Assertions.assertTrue(response.getNeedChangePassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposePasswordExpiredFlagFromMeEndpoint() {
|
||||||
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
|
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
||||||
|
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
|
||||||
|
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN));
|
||||||
|
AuthUserAccountEntity account = activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "SALT-A", 0, null);
|
||||||
|
account.setPasswordChangedAt(LocalDateTime.of(2026, 2, 21, 2, 0));
|
||||||
|
userAccounts.save(account);
|
||||||
|
AuthSessionEntity session = session("token-me-expired-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name());
|
||||||
|
session.setAuthenticatedPrincipalsJson("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"audit-admin-01\"}]");
|
||||||
|
sessions.save(session);
|
||||||
|
|
||||||
|
AuthService service = newAuthService(
|
||||||
|
roleAccounts,
|
||||||
|
userAccounts,
|
||||||
|
new InMemoryAuthFullAccountRepository(),
|
||||||
|
sessions,
|
||||||
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
|
FIXED_CLOCK,
|
||||||
|
() -> "unused"
|
||||||
|
);
|
||||||
|
|
||||||
|
CurrentUserResponse response = service.me(RoleCode.AUDIT_ADMIN.getCode(), "token-me-expired-001");
|
||||||
|
|
||||||
|
Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), response.getRole());
|
||||||
|
Assertions.assertTrue(response.getNeedChangePassword());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldChangeCurrentFullAccountPasswordForActiveSession() {
|
void shouldChangeCurrentFullAccountPasswordForActiveSession() {
|
||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
@ -198,14 +289,15 @@ class AuthServiceTest {
|
|||||||
() -> "unused"
|
() -> "unused"
|
||||||
);
|
);
|
||||||
|
|
||||||
service.changeFullAccountPassword("token-change-001", 1, "12345678", "87654321");
|
service.changeFullAccountPassword("token-change-001", 1, "12345678", "中Abc1234");
|
||||||
|
|
||||||
AuthFullAccountEntity changed = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
|
AuthFullAccountEntity changed = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
|
||||||
Assertions.assertEquals("salt-test", changed.getPasswordSalt());
|
Assertions.assertEquals("salt-test", changed.getPasswordSalt());
|
||||||
Assertions.assertEquals("HASH:87654321:salt-test", changed.getPasswordHash());
|
Assertions.assertEquals("HASH:中Abc1234:salt-test", changed.getPasswordHash());
|
||||||
Assertions.assertEquals(0, changed.getFailedCount());
|
Assertions.assertEquals(0, changed.getFailedCount());
|
||||||
Assertions.assertNull(changed.getLockedUntil());
|
Assertions.assertNull(changed.getLockedUntil());
|
||||||
Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword()));
|
Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword()));
|
||||||
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getPasswordChangedAt());
|
||||||
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastLoginAt());
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastLoginAt());
|
||||||
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt());
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt());
|
||||||
}
|
}
|
||||||
@ -231,18 +323,48 @@ class AuthServiceTest {
|
|||||||
() -> "unused"
|
() -> "unused"
|
||||||
);
|
);
|
||||||
|
|
||||||
service.changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "87654321");
|
service.changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "中Abc1234");
|
||||||
|
|
||||||
AuthUserAccountEntity changed = userAccounts.findByUsername("audit-admin-01").orElseThrow();
|
AuthUserAccountEntity changed = userAccounts.findByUsername("audit-admin-01").orElseThrow();
|
||||||
Assertions.assertEquals("salt-test", changed.getPasswordSalt());
|
Assertions.assertEquals("salt-test", changed.getPasswordSalt());
|
||||||
Assertions.assertEquals("HASH:87654321:salt-test", changed.getPasswordHash());
|
Assertions.assertEquals("HASH:中Abc1234:salt-test", changed.getPasswordHash());
|
||||||
Assertions.assertEquals(0, changed.getFailedCount());
|
Assertions.assertEquals(0, changed.getFailedCount());
|
||||||
Assertions.assertNull(changed.getLockedUntil());
|
Assertions.assertNull(changed.getLockedUntil());
|
||||||
Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword()));
|
Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword()));
|
||||||
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getPasswordChangedAt());
|
||||||
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastLoginAt());
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastLoginAt());
|
||||||
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt());
|
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectCurrentPasswordChangeWhenNewPasswordIsTooSimple() {
|
||||||
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
|
InMemoryAuthUserAccountRepository userAccounts = new InMemoryAuthUserAccountRepository();
|
||||||
|
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
|
||||||
|
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN));
|
||||||
|
userAccounts.save(activeUserAccount("audit-admin-01", RoleCode.AUDIT_ADMIN, "12345678", "LIMITED-SALT-A", 0, null, true));
|
||||||
|
AuthSessionEntity session = session("token-change-limited-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name());
|
||||||
|
session.setAuthenticatedPrincipalsJson("[{\"type\":\"LIMITED\",\"uid\":null,\"username\":\"audit-admin-01\"}]");
|
||||||
|
sessions.save(session);
|
||||||
|
|
||||||
|
AuthService service = newAuthService(
|
||||||
|
roleAccounts,
|
||||||
|
userAccounts,
|
||||||
|
new InMemoryAuthFullAccountRepository(),
|
||||||
|
sessions,
|
||||||
|
new InMemoryRoleUkeyBindingRepository(),
|
||||||
|
FIXED_CLOCK,
|
||||||
|
() -> "unused"
|
||||||
|
);
|
||||||
|
|
||||||
|
BizException exception = Assertions.assertThrows(
|
||||||
|
BizException.class,
|
||||||
|
() -> service.changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "87654321")
|
||||||
|
);
|
||||||
|
|
||||||
|
Assertions.assertEquals("密码复杂度不符合要求:需包含中文、英文、数字或特殊字符,长度不少于八位", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldThrowSessionInvalidWhenChangingFullAccountPasswordWithExpiredSession() {
|
void shouldThrowSessionInvalidWhenChangingFullAccountPasswordWithExpiredSession() {
|
||||||
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
|
||||||
@ -266,7 +388,7 @@ class AuthServiceTest {
|
|||||||
);
|
);
|
||||||
|
|
||||||
BizException exception = Assertions.assertThrows(BizException.class,
|
BizException exception = Assertions.assertThrows(BizException.class,
|
||||||
() -> service.changeFullAccountPassword("token-expired-001", 1, "12345678", "87654321"));
|
() -> service.changeFullAccountPassword("token-expired-001", 1, "12345678", "中Abc1234"));
|
||||||
|
|
||||||
Assertions.assertEquals(ErrorCode.SESSION_INVALID.getCode(), exception.getCode());
|
Assertions.assertEquals(ErrorCode.SESSION_INVALID.getCode(), exception.getCode());
|
||||||
Assertions.assertEquals("session expired", exception.getMessage());
|
Assertions.assertEquals("session expired", exception.getMessage());
|
||||||
@ -667,6 +789,7 @@ class AuthServiceTest {
|
|||||||
entity.setPasswordHash("HASH:" + password + ":" + salt);
|
entity.setPasswordHash("HASH:" + password + ":" + salt);
|
||||||
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
entity.setNeedChangePassword(needChangePassword);
|
entity.setNeedChangePassword(needChangePassword);
|
||||||
|
entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 1, 0));
|
||||||
entity.setFailedCount(failedCount);
|
entity.setFailedCount(failedCount);
|
||||||
entity.setLockedUntil(lockedUntil);
|
entity.setLockedUntil(lockedUntil);
|
||||||
return entity;
|
return entity;
|
||||||
@ -704,6 +827,7 @@ class AuthServiceTest {
|
|||||||
entity.setPasswordHash("HASH:" + password + ":" + salt);
|
entity.setPasswordHash("HASH:" + password + ":" + salt);
|
||||||
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
entity.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||||
entity.setNeedChangePassword(needChangePassword);
|
entity.setNeedChangePassword(needChangePassword);
|
||||||
|
entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 1, 0));
|
||||||
entity.setFailedCount(failedCount);
|
entity.setFailedCount(failedCount);
|
||||||
entity.setLockedUntil(lockedUntil);
|
entity.setLockedUntil(lockedUntil);
|
||||||
return entity;
|
return entity;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user