You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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.

问题原因及解决方案

核心问题

  1. 未禁用默认约束违规信息:Hibernate Validator会生成默认的空错误信息(因为@Password注解的message默认是空),与自定义错误信息冲突,导致前端随机显示其中一个。
  2. 校验逻辑提前返回:遇到第一个错误就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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 23:44:55