From 57ebf819a3176bb4feb9c2a401670f7d4f6a5e60 Mon Sep 17 00:00:00 2001 From: waner Date: Sat, 25 Apr 2026 10:37:19 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=94=B9=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/dto/AdminChangePasswordRequest.java | 2 +- .../auth/dto/ChangePasswordRequest.java | 2 +- .../service/PasswordComplexityValidator.java | 52 ++++++++++++------- .../auth/controller/AuthControllerTest.java | 16 +++--- .../auth/service/AuthAdminServiceTest.java | 12 ++--- .../modules/auth/service/AuthServiceTest.java | 12 ++--- .../PasswordComplexityValidatorTest.java | 38 ++++++++++++++ 7 files changed, 94 insertions(+), 40 deletions(-) create mode 100644 src/test/java/com/cisd/tms/modules/auth/service/PasswordComplexityValidatorTest.java 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 4d0ca87..ec52aa1 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 @@ -11,7 +11,7 @@ public class AdminChangePasswordRequest { private String oldPassword; @NotBlank(message = "newPassword is required") - @Schema(description = "新口令", example = "中Abc1234") + @Schema(description = "新口令", example = "Abc1234!") private String newPassword; public String getOldPassword() { 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 e030c71..7ed80e2 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 @@ -13,7 +13,7 @@ public class ChangePasswordRequest { private String oldPassword; @NotBlank(message = "newPassword is required") - @Schema(description = "新口令", example = "中Abc1234") + @Schema(description = "新口令", example = "Abc1234!") private String newPassword; public String getOldPassword() { diff --git a/src/main/java/com/cisd/tms/modules/auth/service/PasswordComplexityValidator.java b/src/main/java/com/cisd/tms/modules/auth/service/PasswordComplexityValidator.java index 185d1ff..64bb296 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/PasswordComplexityValidator.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/PasswordComplexityValidator.java @@ -5,41 +5,57 @@ import com.cisd.tms.common.exception.BizException; public final class PasswordComplexityValidator { - public static final String MESSAGE = "密码复杂度不符合要求:需包含中文、英文、数字或特殊字符,长度不少于八位"; + public static final String MESSAGE = "密码复杂度不符合要求:需由8到16位大小写字母、数字、符号组成,且三类均需包含"; private PasswordComplexityValidator() { } public static void validate(String password) { String value = password == null ? "" : password; - if (value.length() < 8 || !containsChinese(value) || !containsEnglish(value) || !containsDigitOrSpecial(value)) { + if (value.isBlank() + || value.length() < 8 + || value.length() > 16 + || !isAllowedCharacters(value) + || !containsUppercase(value) + || !containsLowercase(value) + || !containsDigit(value) + || !containsSymbol(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 isAllowedCharacters(String value) { + return value.codePoints().allMatch(codePoint -> + isAsciiLetter(codePoint) || isAsciiDigit(codePoint) || isSymbol(codePoint)); } - private static boolean containsEnglish(String value) { - return value.codePoints().anyMatch(codePoint -> - (codePoint >= 'A' && codePoint <= 'Z') || (codePoint >= 'a' && codePoint <= 'z')); + private static boolean containsUppercase(String value) { + return value.codePoints().anyMatch(codePoint -> codePoint >= 'A' && codePoint <= 'Z'); } - private static boolean containsDigitOrSpecial(String value) { - return value.codePoints().anyMatch(codePoint -> - Character.isDigit(codePoint) || isSpecial(codePoint)); + private static boolean containsLowercase(String value) { + return value.codePoints().anyMatch(codePoint -> codePoint >= 'a' && codePoint <= 'z'); } - private static boolean isSpecial(int codePoint) { - return !Character.isWhitespace(codePoint) - && !Character.isDigit(codePoint) - && !isChineseOrEnglish(codePoint); + private static boolean containsDigit(String value) { + return value.codePoints().anyMatch(PasswordComplexityValidator::isAsciiDigit); } - private static boolean isChineseOrEnglish(int codePoint) { - return Character.UnicodeScript.of(codePoint) == Character.UnicodeScript.HAN - || (codePoint >= 'A' && codePoint <= 'Z') - || (codePoint >= 'a' && codePoint <= 'z'); + private static boolean containsSymbol(String value) { + return value.codePoints().anyMatch(PasswordComplexityValidator::isSymbol); + } + + private static boolean isAsciiLetter(int codePoint) { + return (codePoint >= 'A' && codePoint <= 'Z') || (codePoint >= 'a' && codePoint <= 'z'); + } + + private static boolean isAsciiDigit(int codePoint) { + return codePoint >= '0' && codePoint <= '9'; + } + + private static boolean isSymbol(int codePoint) { + return codePoint >= 33 && codePoint <= 126 + && !isAsciiDigit(codePoint) + && !isAsciiLetter(codePoint); } } 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 83af17f..0905ab4 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 @@ -214,13 +214,13 @@ class AuthControllerTest { .content(""" { "oldPassword": "12345678", - "newPassword": "中Abc1234" + "newPassword": "Abc1234!" } """)) .andExpect(status().isOk()) .andExpect(content().string(containsString("\"success\":true"))); - Mockito.verify(authService).changeFullAccountPassword("token-change-001", 1, "12345678", "中Abc1234"); + Mockito.verify(authService).changeFullAccountPassword("token-change-001", 1, "12345678", "Abc1234!"); } @Test @@ -239,13 +239,13 @@ class AuthControllerTest { .content(""" { "oldPassword": "12345678", - "newPassword": "中Abc1234" + "newPassword": "Abc1234!" } """)) .andExpect(status().isOk()) .andExpect(content().string(containsString("\"success\":true"))); - Mockito.verify(authService).changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "中Abc1234"); + Mockito.verify(authService).changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "Abc1234!"); } @Test @@ -312,13 +312,13 @@ class AuthControllerTest { .content(""" { "oldPassword": "12345678", - "newPassword": "中Abc1234" + "newPassword": "Abc1234!" } """)) .andExpect(status().isOk()) .andExpect(content().string(containsString("\"success\":true"))); - Mockito.verify(authAdminService).changeFullAccountPassword("SUPER_ADMIN", "FULL", "KEY_ADMIN", 1, "12345678", "中Abc1234"); + Mockito.verify(authAdminService).changeFullAccountPassword("SUPER_ADMIN", "FULL", "KEY_ADMIN", 1, "12345678", "Abc1234!"); } @Test @@ -338,13 +338,13 @@ class AuthControllerTest { .content(""" { "oldPassword": "12345678", - "newPassword": "中Abc1234" + "newPassword": "Abc1234!" } """)) .andExpect(status().isOk()) .andExpect(content().string(containsString("\"success\":true"))); - Mockito.verify(authAdminService).changeLimitedAccountPassword("SUPER_ADMIN", "FULL", "AUDIT_ADMIN", "audit-admin-01", "12345678", "中Abc1234"); + Mockito.verify(authAdminService).changeLimitedAccountPassword("SUPER_ADMIN", "FULL", "AUDIT_ADMIN", "audit-admin-01", "12345678", "Abc1234!"); } @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 37fdad3..438f8df 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 @@ -159,12 +159,12 @@ class AuthAdminServiceTest { RoleCode.KEY_ADMIN.getCode(), 1, "12345678", - "中Abc1234" + "Abc1234!" ); AuthFullAccountEntity changed = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow(); Assertions.assertEquals("salt-full-admin", changed.getPasswordSalt()); - Assertions.assertEquals("HASH:中Abc1234:salt-full-admin", changed.getPasswordHash()); + Assertions.assertEquals("HASH:Abc1234!:salt-full-admin", changed.getPasswordHash()); Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus()); Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword())); Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), changed.getPasswordChangedAt()); @@ -198,12 +198,12 @@ class AuthAdminServiceTest { RoleCode.AUDIT_ADMIN.getCode(), "audit-admin-01", "12345678", - "中Abc1234" + "Abc1234!" ); AuthUserAccountEntity changed = userAccounts.findByUsername("audit-admin-01").orElseThrow(); Assertions.assertEquals("salt-limited-admin", changed.getPasswordSalt()); - Assertions.assertEquals("HASH:中Abc1234:salt-limited-admin", changed.getPasswordHash()); + Assertions.assertEquals("HASH:Abc1234!:salt-limited-admin", changed.getPasswordHash()); Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), changed.getStatus()); Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword())); Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), changed.getPasswordChangedAt()); @@ -235,7 +235,7 @@ class AuthAdminServiceTest { RoleCode.AUDIT_ADMIN.getCode(), "audit-admin-01", "bad-password", - "中Abc1234" + "Abc1234!" ) ); @@ -273,7 +273,7 @@ class AuthAdminServiceTest { ) ); - Assertions.assertEquals("密码复杂度不符合要求:需包含中文、英文、数字或特殊字符,长度不少于八位", exception.getMessage()); + Assertions.assertEquals("密码复杂度不符合要求:需由8到16位大小写字母、数字、符号组成,且三类均需包含", exception.getMessage()); } @Test 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 b16202d..c18e7f8 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 @@ -289,11 +289,11 @@ class AuthServiceTest { () -> "unused" ); - service.changeFullAccountPassword("token-change-001", 1, "12345678", "中Abc1234"); + service.changeFullAccountPassword("token-change-001", 1, "12345678", "Abc1234!"); AuthFullAccountEntity changed = fullAccounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow(); Assertions.assertEquals("salt-test", changed.getPasswordSalt()); - Assertions.assertEquals("HASH:中Abc1234:salt-test", changed.getPasswordHash()); + Assertions.assertEquals("HASH:Abc1234!:salt-test", changed.getPasswordHash()); Assertions.assertEquals(0, changed.getFailedCount()); Assertions.assertNull(changed.getLockedUntil()); Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword())); @@ -323,11 +323,11 @@ class AuthServiceTest { () -> "unused" ); - service.changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "中Abc1234"); + service.changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "Abc1234!"); AuthUserAccountEntity changed = userAccounts.findByUsername("audit-admin-01").orElseThrow(); Assertions.assertEquals("salt-test", changed.getPasswordSalt()); - Assertions.assertEquals("HASH:中Abc1234:salt-test", changed.getPasswordHash()); + Assertions.assertEquals("HASH:Abc1234!:salt-test", changed.getPasswordHash()); Assertions.assertEquals(0, changed.getFailedCount()); Assertions.assertNull(changed.getLockedUntil()); Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword())); @@ -362,7 +362,7 @@ class AuthServiceTest { () -> service.changeLimitedAccountPassword("token-change-limited-001", "audit-admin-01", "12345678", "87654321") ); - Assertions.assertEquals("密码复杂度不符合要求:需包含中文、英文、数字或特殊字符,长度不少于八位", exception.getMessage()); + Assertions.assertEquals("密码复杂度不符合要求:需由8到16位大小写字母、数字、符号组成,且三类均需包含", exception.getMessage()); } @Test @@ -388,7 +388,7 @@ class AuthServiceTest { ); BizException exception = Assertions.assertThrows(BizException.class, - () -> service.changeFullAccountPassword("token-expired-001", 1, "12345678", "中Abc1234")); + () -> service.changeFullAccountPassword("token-expired-001", 1, "12345678", "Abc1234!")); Assertions.assertEquals(ErrorCode.SESSION_INVALID.getCode(), exception.getCode()); Assertions.assertEquals("session expired", exception.getMessage()); diff --git a/src/test/java/com/cisd/tms/modules/auth/service/PasswordComplexityValidatorTest.java b/src/test/java/com/cisd/tms/modules/auth/service/PasswordComplexityValidatorTest.java new file mode 100644 index 0000000..48c13b4 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/auth/service/PasswordComplexityValidatorTest.java @@ -0,0 +1,38 @@ +package com.cisd.tms.modules.auth.service; + +import com.cisd.tms.common.exception.BizException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class PasswordComplexityValidatorTest { + + @Test + void shouldAcceptPasswordWithLettersDigitsAndSymbolInAllowedLength() { + Assertions.assertDoesNotThrow(() -> PasswordComplexityValidator.validate("Abc1234!")); + Assertions.assertDoesNotThrow(() -> PasswordComplexityValidator.validate("Aa123456789012!")); + } + + @Test + void shouldRejectPasswordOutsideNewComplexityPolicy() { + assertInvalid(null); + assertInvalid(""); + assertInvalid(" "); + assertInvalid("Abc123!"); + assertInvalid("Abc1234567890123!"); + assertInvalid("abc1234!"); + assertInvalid("ABC1234!"); + assertInvalid("Abcdefg!"); + assertInvalid("Abc12345"); + assertInvalid("中Abc1234!"); + assertInvalid("Abc1234!"); + assertInvalid("Abc 123!"); + } + + private void assertInvalid(String password) { + BizException exception = Assertions.assertThrows( + BizException.class, + () -> PasswordComplexityValidator.validate(password) + ); + Assertions.assertEquals(PasswordComplexityValidator.MESSAGE, exception.getMessage()); + } +}