From 505a47d0480c6ee9409a6eaf35a415e4b375d3a8 Mon Sep 17 00:00:00 2001 From: wanghu Date: Mon, 24 Aug 2026 18:28:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E5=85=A5=E5=90=8C=E6=AD=A5=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validation/PasswordCheckable.java | 14 +++ .../validation/PasswordValidation.java | 22 +++++ .../PasswordValidationValidator.java | 88 +++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 src/main/java/com/yunhai/bschclassmanage/validation/PasswordCheckable.java create mode 100644 src/main/java/com/yunhai/bschclassmanage/validation/PasswordValidation.java create mode 100644 src/main/java/com/yunhai/bschclassmanage/validation/PasswordValidationValidator.java diff --git a/src/main/java/com/yunhai/bschclassmanage/validation/PasswordCheckable.java b/src/main/java/com/yunhai/bschclassmanage/validation/PasswordCheckable.java new file mode 100644 index 0000000..2bb5ebc --- /dev/null +++ b/src/main/java/com/yunhai/bschclassmanage/validation/PasswordCheckable.java @@ -0,0 +1,14 @@ +package com.yunhai.bschclassmanage.validation; + +/** + * 密码校验通用契约:供类级 @PasswordValidation 校验器读取 + * 密码值与业务主键 id(判断新增/更新),学生/教师等 DTO 实现之。 + */ +public interface PasswordCheckable { + + /** 密码值 */ + String getPwd(); + + /** 业务主键 id:null 或 <=0 视为新增,>0 视为更新 */ + Integer getId(); +} diff --git a/src/main/java/com/yunhai/bschclassmanage/validation/PasswordValidation.java b/src/main/java/com/yunhai/bschclassmanage/validation/PasswordValidation.java new file mode 100644 index 0000000..8f60c8b --- /dev/null +++ b/src/main/java/com/yunhai/bschclassmanage/validation/PasswordValidation.java @@ -0,0 +1,22 @@ +package com.yunhai.bschclassmanage.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.*; + +/** + * 类级密码校验(作用于 SchStudentDTO): + * - id 为 null 或 <=0(新增场景):密码必填且必须通过全部强度规则; + * - id > 0(更新场景):密码为空则跳过;密码非空则同样必须通过全部强度规则。 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = PasswordValidationValidator.class) +@Documented +public @interface PasswordValidation { + String message() default "密码校验未通过"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/yunhai/bschclassmanage/validation/PasswordValidationValidator.java b/src/main/java/com/yunhai/bschclassmanage/validation/PasswordValidationValidator.java new file mode 100644 index 0000000..d0f276e --- /dev/null +++ b/src/main/java/com/yunhai/bschclassmanage/validation/PasswordValidationValidator.java @@ -0,0 +1,88 @@ +package com.yunhai.bschclassmanage.validation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +/** + * 类级校验器:依据 id 判断是否需要校验密码(适用于学生/教师等实现 PasswordCheckable 的 DTO)。 + * 新增(id == null 或 id <= 0):密码必填且必须满足强度规则; + * 更新(id > 0):密码为空跳过,密码非空同样必须满足强度规则。 + * 各类违规均以独立消息挂在 pwd 属性上返回。 + */ +public class PasswordValidationValidator implements ConstraintValidator { + + private static final int MIN_LEN = 8; + private static final int MAX_LEN = 32; + /** 连续数字判定的最小位数(升序 1234 / 降序 4321) */ + private static final int MIN_RUN = 4; + + private static final Pattern COMPOSE = + Pattern.compile("^(?=.*[A-Za-z])(?=.*\\d)(?=.*[^A-Za-z0-9]).+$"); + + @Override + public boolean isValid(PasswordCheckable dto, ConstraintValidatorContext context) { + Integer id = dto.getId(); + boolean isNew = (id == null || id <= 0); + + String pwd = dto.getPwd(); + // 密码为空:仅新增场景报错;更新场景(id>0)跳过 + if (pwd == null || pwd.isEmpty()) { + if (isNew) { + addViolation(context, "新增时密码不允许为空"); + return false; + } + return true; + } + + // 密码非空:新增与更新都必须满足强度规则 + boolean valid = true; + if (pwd.length() < MIN_LEN || pwd.length() > MAX_LEN) { + addViolation(context, "密码长度必须为8-32位"); + valid = false; + } + if (!COMPOSE.matcher(pwd).matches()) { + addViolation(context, "密码必须同时包含字母、数字和符号"); + valid = false; + } + if (containsConsecutiveSequence(pwd)) { + addViolation(context, "密码不允许包含连续自然数序列(如1234、6789)"); + valid = false; + } + return valid; + } + + private void addViolation(ConstraintValidatorContext context, String msg) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(msg) + .addPropertyNode("pwd") + .addConstraintViolation(); + } + + private boolean containsConsecutiveSequence(String s) { + int n = s.length(); + for (int i = 0; i + MIN_RUN <= n; i++) { + boolean asc = true; // 升序:1234 + boolean desc = true; // 降序:4321 + for (int j = 0; j < MIN_RUN - 1; j++) { + int a = s.charAt(i + j); + int b = s.charAt(i + j + 1); + if (!Character.isDigit(a) || !Character.isDigit(b)) { + asc = false; + desc = false; + break; + } + if (b - a != 1) { + asc = false; + } + if (a - b != 1) { + desc = false; + } + } + if (asc || desc) { + return true; + } + } + return false; + } +}