Spring Boot自定义Password校验器在模板中随机显示错误问题
Spring Boot自定义密码校验注解错误信息随机显示问题
在Spring Boot项目中实现了自定义@Password校验注解及校验器,要求密码满足至少8位、包含大小写字母、数字和符号等规则,且每个违规规则对应独立错误提示。但测试发现Thymeleaf前端的密码错误信息随机显示——同一错误密码有时能看到提示,有时看不到。
相关代码
1. RegisterDTO.java(包含@Password注解)
@Data @ConfirmPassword public class RegisterDTO { @NotBlank(message = "{email.NotBlank}") @Email(message = "{email.Email}") private String email; @NotBlank(message = "{password.NotBlank}") @Password private String password; private String password2; @NotBlank(message = "{phone.NotBlank}") @Pattern(regexp = "^\\+381 \\d{2} \\d{6,7}$", message = "{phone.Pattern}", groups = AdvancedValidation.class) // ... 其他字段 }
2. Password.java(自定义校验注解)
@Documented @Constraint(validatedBy = PasswordConstraint.class) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) public @interface Password { String message() default ""; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
3. PasswordConstraint.java(校验器实现)
public class PasswordConstraint implements ConstraintValidator<Password, String> { @Override public void initialize(Password password) { ConstraintValidator.super.initialize(password); } private boolean min(String password) { int min = 8; return password.length() < min; } private boolean mixed(String password) { boolean upper = password.chars().anyMatch(c -> Character.isAlphabetic(c) && Character.isUpperCase(c)); boolean lower = password.chars().anyMatch(c -> Character.isAlphabetic(c) && Character.isLowerCase(c)); return !(upper && lower); } private boolean numbers(String password) { return password.chars().noneMatch(Character::isDigit); } private boolean symbols(String password) { String symbols = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; return password.chars().noneMatch(c -> symbols.indexOf(c) >= 0); } @Override public boolean isValid(String password, ConstraintValidatorContext context) { if (password == null || password.isEmpty()) return true; if (min(password)) { // password.Min context.buildConstraintViolationWithTemplate("{password.Min}").addConstraintViolation(); return false; } if (mixed(password)) { // password.Mixed context.buildConstraintViolationWithTemplate("{password.Mixed}").addConstraintViolation(); return false; } if (numbers(password)) { // password.Numbers context.buildConstraintViolationWithTemplate("{password.Numbers}").addConstraintViolation(); return false; } if (symbols(password)) { // password.Symbols context.buildConstraintViolationWithTemplate("{password.Symbols}").addConstraintViolation(); return false; } return true; } }
4. Thymeleaf模板(register.html)
<form class="border border-secondary-subtle rounded-3 p-3 mb-3" th:action="@{/registracija}" th:object="${dto}" method="post" novalidate> <h1 class="fw-bold mb-3">Registracija</h1> <h5 class="mb-3"><a class="fw-semibold" th:href="@{/prijava}">Prijavite se</a></h5> <!-- CSRF token --> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> <div class="row gy-3 mb-3"> <!-- ... 其他字段 --> <div class="col-12"> <div class="input-group"> <div class="form-floating"> <input class="form-control" type="password" id="password" th:field="*{password}"> <label for="password">Lozinka</label> </div> <div class="form-floating"> <input class="form-control" type="password" id="password2" th:field="*{password2}"> <label for="password2">Potvrdi lozinku</label> </div> </div> <div class="form-text text-danger" th:if="${#fields.hasErrors('password')}" th:text="${#fields.errors('password')[0]}"></div> <div class="form-text text-danger" th:if="${#fields.hasErrors('password2')}" th:text="${#fields.errors('password2')[0]}"></div> </div> </form>
5. 国际化配置(messages.properties)
# User birthDate.NotNull =Polje datum rođenja je obavezno. birthDate.Adult =Osoba mora biti punoletna. email.NotBlank =Polje imejl adresa je obavezno. email.Email =Polje imejl adresa mora biti ispravna imejl adresa. firstName.NotBlank =Polje ime je obavezno. firstName.Alpha =Polje ime mora sadržati samo slova. lastName.NotBlank =Polje prezime je obavezno. lastName.Alpha =Polje prezime mora sadržati samo slova. password.NotBlank =Polje lozinka je obavezno. password.Letters =Polje lozinka mora sadržati bar jedno slovo. password.Min =Polje lozinka mora imati bar 8 karaktera. password.Mixed =Polje lozinka mora sadržati bar jedno veliko i bar jedno malo slovo. password.Numbers =Polje lozinka mora sadržati bar jedan broj. password.Symbols =Polje lozinka mora sadržati bar jedan znak. password2.NotBlank =Polje potvrda lozinke je obavezno. password2.ConfirmPassword =Potvrda polja lozinka se ne podudara. phone.NotBlank =Polje broj telefona je obavezno. phone.Pattern =Polje broj telefona mora biti ispravan broj telefona.
问题原因及解决方案
核心问题
- 未禁用默认约束违规信息:Hibernate Validator会生成默认的空错误信息(因为
@Password注解的message默认是空),与自定义错误信息冲突,导致前端随机显示其中一个。 - 校验逻辑提前返回:遇到第一个错误就
return false,虽然只会生成一个错误,但默认约束信息的干扰仍会导致显示异常。
修改后的校验器代码
@Override public boolean isValid(String password, ConstraintValidatorContext context) { if (password == null || password.isEmpty()) { return true; } // 禁用默认约束错误信息,避免与自定义提示冲突 context.disableDefaultConstraintViolation(); boolean isValid = true; if (min(password)) { context.buildConstraintViolationWithTemplate("{password.Min}").addConstraintViolation(); isValid = false; } if (mixed(password)) { context.buildConstraintViolationWithTemplate("{password.Mixed}").addConstraintViolation(); isValid = false; } if (numbers(password)) { context.buildConstraintViolationWithTemplate("{password.Numbers}").addConstraintViolation(); isValid = false; } if (symbols(password)) { context.buildConstraintViolationWithTemplate("{password.Symbols}").addConstraintViolation(); isValid = false; } return isValid; }
可选优化(前端显示所有错误)
如果需要显示所有违规规则的错误信息,可修改Thymeleaf模板,遍历所有错误:
<div class="form-text text-danger" th:if="${#fields.hasErrors('password')}"> <ul class="mb-0"> <li th:each="error : ${#fields.errors('password')}" th:text="${error}"></li> </ul> </div>
内容的提问来源于stack exchange,提问作者IgorArnaut
相关产品推荐
相关产品推荐

