feat:密码改密复杂度要求和定期30天过期策略
This commit is contained in:
@@ -11,7 +11,7 @@ public class AdminChangePasswordRequest {
|
||||
private String oldPassword;
|
||||
|
||||
@NotBlank(message = "newPassword is required")
|
||||
@Schema(description = "新口令", example = "87654321")
|
||||
@Schema(description = "新口令", example = "中Abc1234")
|
||||
private String newPassword;
|
||||
|
||||
public String getOldPassword() {
|
||||
|
||||
@@ -13,7 +13,7 @@ public class ChangePasswordRequest {
|
||||
private String oldPassword;
|
||||
|
||||
@NotBlank(message = "newPassword is required")
|
||||
@Schema(description = "新口令", example = "87654321")
|
||||
@Schema(description = "新口令", example = "中Abc1234")
|
||||
private String newPassword;
|
||||
|
||||
public String getOldPassword() {
|
||||
|
||||
@@ -15,6 +15,7 @@ public class AuthFullAccountEntity extends BaseEntity {
|
||||
private String passwordSalt;
|
||||
private String status;
|
||||
private Boolean needChangePassword;
|
||||
private LocalDateTime passwordChangedAt;
|
||||
private Integer failedCount;
|
||||
private LocalDateTime lockedUntil;
|
||||
private LocalDateTime lastLoginAt;
|
||||
@@ -84,6 +85,14 @@ public class AuthFullAccountEntity extends BaseEntity {
|
||||
this.needChangePassword = needChangePassword;
|
||||
}
|
||||
|
||||
public LocalDateTime getPasswordChangedAt() {
|
||||
return passwordChangedAt;
|
||||
}
|
||||
|
||||
public void setPasswordChangedAt(LocalDateTime passwordChangedAt) {
|
||||
this.passwordChangedAt = passwordChangedAt;
|
||||
}
|
||||
|
||||
public Integer getFailedCount() {
|
||||
return failedCount;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public class AuthUserAccountEntity extends BaseEntity {
|
||||
private String passwordSalt;
|
||||
private String status;
|
||||
private Boolean needChangePassword;
|
||||
private LocalDateTime passwordChangedAt;
|
||||
private Integer failedCount;
|
||||
private LocalDateTime lockedUntil;
|
||||
private LocalDateTime lastLoginAt;
|
||||
@@ -75,6 +76,14 @@ public class AuthUserAccountEntity extends BaseEntity {
|
||||
this.needChangePassword = needChangePassword;
|
||||
}
|
||||
|
||||
public LocalDateTime getPasswordChangedAt() {
|
||||
return passwordChangedAt;
|
||||
}
|
||||
|
||||
public void setPasswordChangedAt(LocalDateTime passwordChangedAt) {
|
||||
this.passwordChangedAt = passwordChangedAt;
|
||||
}
|
||||
|
||||
public Integer getFailedCount() {
|
||||
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.RoleUkeyBindingRepository;
|
||||
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.PasswordSaltGenerator;
|
||||
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
|
||||
@@ -87,6 +88,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
||||
RoleAccountEntity target = loadRole(targetRoleCode);
|
||||
target.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||
roleAccountRepository.update(target);
|
||||
LocalDateTime current = now();
|
||||
|
||||
List<AuthFullAccountEntity> fullAccounts = authFullAccountRepository.findByRoleCode(targetRoleCode);
|
||||
if (fullAccounts.isEmpty()) {
|
||||
@@ -98,6 +100,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
||||
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
|
||||
account.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||
account.setNeedChangePassword(Boolean.TRUE);
|
||||
account.setPasswordChangedAt(current);
|
||||
account.setFailedCount(0);
|
||||
account.setLockedUntil(null);
|
||||
account.setLastActiveAt(null);
|
||||
@@ -115,6 +118,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
||||
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt));
|
||||
account.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||
account.setNeedChangePassword(Boolean.TRUE);
|
||||
account.setPasswordChangedAt(current);
|
||||
account.setFailedCount(0);
|
||||
account.setLockedUntil(null);
|
||||
account.setLastActiveAt(null);
|
||||
@@ -132,6 +136,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
||||
String oldPassword,
|
||||
String newPassword
|
||||
) {
|
||||
PasswordComplexityValidator.validate(newPassword);
|
||||
loadRole(targetRoleCode);
|
||||
RoleCode targetRole = resolveRoleCode(targetRoleCode);
|
||||
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
|
||||
@@ -155,6 +160,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
||||
String oldPassword,
|
||||
String newPassword
|
||||
) {
|
||||
PasswordComplexityValidator.validate(newPassword);
|
||||
loadRole(targetRoleCode);
|
||||
AuthUserAccountEntity account = authUserAccountRepository.findByUsername(username)
|
||||
.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.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
|
||||
account.setNeedChangePassword(Boolean.TRUE);
|
||||
account.setPasswordChangedAt(now());
|
||||
account.setFailedCount(0);
|
||||
account.setLockedUntil(null);
|
||||
if (RoleAccountStatus.LOCKED.name().equals(account.getStatus())) {
|
||||
@@ -274,6 +281,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
|
||||
account.setPasswordSalt(newSalt);
|
||||
account.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
|
||||
account.setNeedChangePassword(Boolean.TRUE);
|
||||
account.setPasswordChangedAt(now());
|
||||
account.setFailedCount(0);
|
||||
account.setLockedUntil(null);
|
||||
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.CompatUkeyVerifier;
|
||||
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.SessionTokenGenerator;
|
||||
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 IDLE_TIMEOUT_MINUTES = 10;
|
||||
private static final int PASSWORD_EXPIRE_DAYS = 30;
|
||||
|
||||
private final RoleAccountRepository roleAccountRepository;
|
||||
private final AuthUserRepository authUserRepository;
|
||||
@@ -236,6 +238,7 @@ public class AuthServiceImpl implements AuthService {
|
||||
|
||||
@Override
|
||||
public void changeFullAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword) {
|
||||
PasswordComplexityValidator.validate(newPassword);
|
||||
AuthSessionEntity session = requireActiveSession(sessionToken);
|
||||
if (!AuthLevel.FULL.name().equals(session.getAuthLevel())) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full session is required");
|
||||
@@ -258,6 +261,7 @@ public class AuthServiceImpl implements AuthService {
|
||||
fullAccount.setLockedUntil(null);
|
||||
fullAccount.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||
fullAccount.setNeedChangePassword(Boolean.FALSE);
|
||||
fullAccount.setPasswordChangedAt(current);
|
||||
fullAccount.setLastLoginAt(current);
|
||||
fullAccount.setLastActiveAt(current);
|
||||
authFullAccountRepository.update(fullAccount);
|
||||
@@ -269,6 +273,7 @@ public class AuthServiceImpl implements AuthService {
|
||||
|
||||
@Override
|
||||
public void changeLimitedAccountPassword(String sessionToken, String username, String currentPassword, String newPassword) {
|
||||
PasswordComplexityValidator.validate(newPassword);
|
||||
AuthSessionEntity session = requireActiveSession(sessionToken);
|
||||
if (!AuthLevel.LIMITED.name().equals(session.getAuthLevel())) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "limited session is required");
|
||||
@@ -292,6 +297,7 @@ public class AuthServiceImpl implements AuthService {
|
||||
userAccount.setLockedUntil(null);
|
||||
userAccount.setStatus(RoleAccountStatus.ACTIVE.name());
|
||||
userAccount.setNeedChangePassword(Boolean.FALSE);
|
||||
userAccount.setPasswordChangedAt(current);
|
||||
userAccount.setLastLoginAt(current);
|
||||
userAccount.setLastActiveAt(current);
|
||||
authUserAccountRepository.update(userAccount);
|
||||
@@ -652,11 +658,11 @@ public class AuthServiceImpl implements AuthService {
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
@@ -672,14 +678,28 @@ public class AuthServiceImpl implements AuthService {
|
||||
return accounts.stream()
|
||||
.filter(account -> principals.stream()
|
||||
.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());
|
||||
return accounts.stream()
|
||||
.filter(account -> principals.stream()
|
||||
.anyMatch(principal -> "LIMITED".equals(principal.getType())
|
||||
&& 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) {
|
||||
|
||||
@@ -275,6 +275,7 @@ CREATE TABLE IF NOT EXISTS tms_auth_user_account (
|
||||
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,
|
||||
@@ -295,6 +296,7 @@ CREATE TABLE IF NOT EXISTS tms_auth_full_account (
|
||||
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,
|
||||
@@ -445,6 +447,7 @@ INSERT IGNORE INTO tms_auth_user_account (
|
||||
password_salt,
|
||||
status,
|
||||
need_change_password,
|
||||
password_changed_at,
|
||||
failed_count,
|
||||
locked_until,
|
||||
last_login_at,
|
||||
@@ -462,6 +465,7 @@ VALUES
|
||||
'init-super-admin-salt-20260325',
|
||||
'ACTIVE',
|
||||
1,
|
||||
CURRENT_TIMESTAMP(3),
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -478,6 +482,7 @@ VALUES
|
||||
'init-super-admin-salt-20260325',
|
||||
'ACTIVE',
|
||||
1,
|
||||
CURRENT_TIMESTAMP(3),
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -494,6 +499,7 @@ VALUES
|
||||
'init-key-admin-salt-20260325',
|
||||
'ACTIVE',
|
||||
1,
|
||||
CURRENT_TIMESTAMP(3),
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -510,6 +516,7 @@ VALUES
|
||||
'init-audit-admin-salt-20260325',
|
||||
'ACTIVE',
|
||||
1,
|
||||
CURRENT_TIMESTAMP(3),
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -526,6 +533,7 @@ VALUES
|
||||
'init-ops-admin-salt-20260325',
|
||||
'ACTIVE',
|
||||
1,
|
||||
CURRENT_TIMESTAMP(3),
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -544,6 +552,7 @@ INSERT IGNORE INTO tms_auth_full_account (
|
||||
password_salt,
|
||||
status,
|
||||
need_change_password,
|
||||
password_changed_at,
|
||||
failed_count,
|
||||
locked_until,
|
||||
last_login_at,
|
||||
@@ -562,6 +571,7 @@ VALUES
|
||||
'init-super-admin-salt-20260325',
|
||||
'ACTIVE',
|
||||
1,
|
||||
CURRENT_TIMESTAMP(3),
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -579,6 +589,7 @@ VALUES
|
||||
'init-super-admin-salt-20260325',
|
||||
'ACTIVE',
|
||||
1,
|
||||
CURRENT_TIMESTAMP(3),
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -596,6 +607,7 @@ VALUES
|
||||
'init-key-admin-salt-20260325',
|
||||
'ACTIVE',
|
||||
1,
|
||||
CURRENT_TIMESTAMP(3),
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -613,6 +625,7 @@ VALUES
|
||||
'init-audit-admin-salt-20260325',
|
||||
'ACTIVE',
|
||||
1,
|
||||
CURRENT_TIMESTAMP(3),
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -630,6 +643,7 @@ VALUES
|
||||
'init-ops-admin-salt-20260325',
|
||||
'ACTIVE',
|
||||
1,
|
||||
CURRENT_TIMESTAMP(3),
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<result property="passwordSalt" column="password_salt"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="needChangePassword" column="need_change_password"/>
|
||||
<result property="passwordChangedAt" column="password_changed_at"/>
|
||||
<result property="failedCount" column="failed_count"/>
|
||||
<result property="lockedUntil" column="locked_until"/>
|
||||
<result property="lastLoginAt" column="last_login_at"/>
|
||||
@@ -32,6 +33,7 @@
|
||||
password_salt,
|
||||
status,
|
||||
need_change_password,
|
||||
password_changed_at,
|
||||
failed_count,
|
||||
locked_until,
|
||||
last_login_at,
|
||||
@@ -54,6 +56,7 @@
|
||||
password_salt,
|
||||
status,
|
||||
need_change_password,
|
||||
password_changed_at,
|
||||
failed_count,
|
||||
locked_until,
|
||||
last_login_at,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<result property="passwordSalt" column="password_salt"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="needChangePassword" column="need_change_password"/>
|
||||
<result property="passwordChangedAt" column="password_changed_at"/>
|
||||
<result property="failedCount" column="failed_count"/>
|
||||
<result property="lockedUntil" column="locked_until"/>
|
||||
<result property="lastLoginAt" column="last_login_at"/>
|
||||
@@ -30,6 +31,7 @@
|
||||
password_salt,
|
||||
status,
|
||||
need_change_password,
|
||||
password_changed_at,
|
||||
failed_count,
|
||||
locked_until,
|
||||
last_login_at,
|
||||
@@ -50,6 +52,7 @@
|
||||
password_salt,
|
||||
status,
|
||||
need_change_password,
|
||||
password_changed_at,
|
||||
failed_count,
|
||||
locked_until,
|
||||
last_login_at,
|
||||
|
||||
Reference in New Issue
Block a user