Java框架快速入门: Spring Security+OAuth2之密码验证规则与自定义注解

概述

在企业级安全开发中,密码策略往往非常复杂:禁止连续字母、连续数字、键盘序列、必须包含大小写与特殊字符,同时密码和确认密码必须一致。

如果直接在业务逻辑中硬编码这些规则,代码会迅速腐化。本文将介绍如何基于 Passay 密码策略库与 Jakarta Bean Validation 自定义注解,将密码验证逻辑彻底从业务代码中剥离,并实现类级别的复合属性校验。

纲要

  • Passay 框架简介与依赖引入
  • 单个属性校验:@PasswordConstraint 注解与验证器
    • 使用 PasswordValidator 组装多重规则
    • 规则:长度、大写、小写、特殊字符、禁止连续序列、禁止键盘连续序列
  • 多属性复合校验:@PasswordMatch 类级别注解
    • 校验 passwordconfirmPassword 一致性
  • DTO 层应用示例
  • 自定义验证流程可视化

Passay:专业密码策略库

Passay 提供了丰富的预置规则(Rule),并允许通过 PasswordValidator 灵活组装。相比于手写正则,它更易读、易维护,且对连续序列、键盘序列等复杂规则提供了开箱即用的支持。

引入依赖

pom.xml 中添加 passayspring-boot-starter-validation(后者提供验证框架支持):

xml 复制代码
<properties>
    <passay.version>1.6.0</passay.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.passay</groupId>
        <artifactId>passay</artifactId>
        <version>${passay.version}</version>
    </dependency>
</dependencies>

自定义密码验证注解 @PasswordConstraint

为了让密码规则注解化,我们首先定义一个作用于字段的约束注解。

java 复制代码
package com.example.validation.annotation;

import com.example.validation.validator.PasswordConstraintValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = PasswordConstraintValidator.class)
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PasswordConstraint {
    String message() default "密码不符合安全规则";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

验证器实现:引入 Passay 规则

验证器内部使用 PasswordValidator 组合规则,包括:

  • 长度:8~30 位
  • 字符规则:至少一个大写字母、一个小写字母、一个特殊字符
  • 非连续序列:不允许 5 个连续字母或数字
  • 键盘序列:不允许 QWERTY 键盘上的 5 个连续字符(如 qwert
java 复制代码
package com.example.validation.validator;

import com.example.validation.annotation.PasswordConstraint;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.passay.*;

import java.util.Arrays;

public class PasswordConstraintValidator implements ConstraintValidator<PasswordConstraint, String> {

    private final PasswordValidator passwordValidator;

    public PasswordConstraintValidator() {
        this.passwordValidator = new PasswordValidator(Arrays.asList(
                // 长度规则 8-30
                new LengthRule(8, 30),
                // 至少一个大写字母
                new CharacterRule(EnglishCharacterData.UpperCase, 1),
                // 至少一个小写字母
                new CharacterRule(EnglishCharacterData.LowerCase, 1),
                // 至少一个特殊字符
                new CharacterRule(EnglishCharacterData.Special, 1),
                // 不允许5个连续字母
                new IllegalSequenceRule(EnglishSequenceData.Alphabetical, 5, false),
                // 不允许5个连续数字
                new IllegalSequenceRule(EnglishSequenceData.Numerical, 5, false),
                // 不允许QWERTY键盘序列5个连续
                new IllegalSequenceRule(EnglishSequenceData.USQwerty, 5, false),
                // 不允许包含空白字符
                new WhitespaceRule()
        ));
    }

    @Override
    public boolean isValid(String password, ConstraintValidatorContext context) {
        if (password == null) {
            return false;
        }
        RuleResult result = passwordValidator.validate(new PasswordData(password));
        return result.isValid();
    }
}

类级别复合校验 @PasswordMatch

密码与确认密码必须相等,这属于类中两个属性的复合校验。我们定义类级别的注解 @PasswordMatch

java 复制代码
package com.example.validation.annotation;

import com.example.validation.validator.PasswordMatchValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = PasswordMatchValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PasswordMatch {
    String message() default "密码与确认密码不一致";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

对应的验证器通过反射获取 passwordconfirmPassword 字段值进行比较:

java 复制代码
package com.example.validation.validator;

import com.example.validation.annotation.PasswordMatch;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.BeanWrapperImpl;

public class PasswordMatchValidator implements ConstraintValidator<PasswordMatch, Object> {

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        Object password = new BeanWrapperImpl(value).getPropertyValue("password");
        Object confirmPassword = new BeanWrapperImpl(value).getPropertyValue("confirmPassword");
        if (password == null) {
            return false;
        }
        return password.equals(confirmPassword);
    }
}

DTO 应用与验证流程

将两个注解应用于 UserDto,其中 @PasswordMatch 作用于类上,@PasswordConstraint 作用于密码字段。

java 复制代码
package com.example.dto;

import com.example.validation.annotation.PasswordConstraint;
import com.example.validation.annotation.PasswordMatch;
import jakarta.validation.constraints.Email;
import lombok.Data;

@Data
@PasswordMatch
public class UserDto {

    @Email(message = "邮箱格式不正确")
    private String email;

    @PasswordConstraint
    private String password;

    private String confirmPassword;
}

当请求进入 Controller 并使用 @Valid 触发校验时,执行流程如下:
Passay Validator Controller Client Passay Validator Controller Client #mermaid-svg-GRNoe8OJH5KIsYRO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GRNoe8OJH5KIsYRO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GRNoe8OJH5KIsYRO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GRNoe8OJH5KIsYRO .error-icon{fill:#552222;}#mermaid-svg-GRNoe8OJH5KIsYRO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GRNoe8OJH5KIsYRO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GRNoe8OJH5KIsYRO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GRNoe8OJH5KIsYRO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GRNoe8OJH5KIsYRO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GRNoe8OJH5KIsYRO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GRNoe8OJH5KIsYRO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GRNoe8OJH5KIsYRO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GRNoe8OJH5KIsYRO .marker.cross{stroke:#333333;}#mermaid-svg-GRNoe8OJH5KIsYRO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GRNoe8OJH5KIsYRO p{margin:0;}#mermaid-svg-GRNoe8OJH5KIsYRO .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GRNoe8OJH5KIsYRO text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-GRNoe8OJH5KIsYRO .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GRNoe8OJH5KIsYRO .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-GRNoe8OJH5KIsYRO .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-GRNoe8OJH5KIsYRO .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-GRNoe8OJH5KIsYRO #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-GRNoe8OJH5KIsYRO .sequenceNumber{fill:white;}#mermaid-svg-GRNoe8OJH5KIsYRO #sequencenumber{fill:#333;}#mermaid-svg-GRNoe8OJH5KIsYRO #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-GRNoe8OJH5KIsYRO .messageText{fill:#333;stroke:none;}#mermaid-svg-GRNoe8OJH5KIsYRO .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GRNoe8OJH5KIsYRO .labelText,#mermaid-svg-GRNoe8OJH5KIsYRO .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-GRNoe8OJH5KIsYRO .loopText,#mermaid-svg-GRNoe8OJH5KIsYRO .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-GRNoe8OJH5KIsYRO .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GRNoe8OJH5KIsYRO .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-GRNoe8OJH5KIsYRO .noteText,#mermaid-svg-GRNoe8OJH5KIsYRO .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-GRNoe8OJH5KIsYRO .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GRNoe8OJH5KIsYRO .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GRNoe8OJH5KIsYRO .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GRNoe8OJH5KIsYRO .actorPopupMenu{position:absolute;}#mermaid-svg-GRNoe8OJH5KIsYRO .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-GRNoe8OJH5KIsYRO .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GRNoe8OJH5KIsYRO .actor-man circle,#mermaid-svg-GRNoe8OJH5KIsYRO line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-GRNoe8OJH5KIsYRO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 委托 PasswordConstraintValidator 比较 password 与 confirmPassword alt所有约束通过存在违规 POST /register (UserDto)validate(UserDto)校验 @Email校验 @PasswordConstraintPasswordValidator.validate(password)RuleResult校验 @PasswordMatch (类级别)无违规注册成功ConstraintViolation 集合400 错误详情

项目结构概览

dir 复制代码
src/main/java/com/example/
├── dto
│   └── UserDto.java
├── validation
│   ├── annotation
│   │   ├── PasswordConstraint.java
│   │   └── PasswordMatch.java
│   └── validator
│       ├── PasswordConstraintValidator.java
│       └── PasswordMatchValidator.java

总结

通过 Passay 与自定义注解的结合,我们将复杂的密码策略封装为声明式校验,完全解耦业务代码。同时演示了类级别复合验证如何优雅地处理 "密码与确认密码" 的场景。

这套模式可扩展至任何需要多属性组合校验的 DTO,有效提高代码可读性与安全性。

相关推荐
一嘴一个橘子1 小时前
java - redis 缓存击穿 - 互斥锁
java
shehuiyuelaiyuehao1 小时前
算法39,位运算,消失的两个数字
java·数据结构·算法
小刘在重生~1 小时前
Java 异常体系完整笔记
java·笔记·python
抠脚小弟1 小时前
Spring Task 定时任务详解:从入门到实战
java·后端·spring
少陽君1 小时前
Python 并发编程:IO 等待用线程/asyncio,CPU 计算才用进程
数据库·python·php
这个DBA有点耶2 小时前
VLDB 2026核心议题解读:当负载被AI改写,数据库的内核该往哪走?
数据库·架构·aigc
2601_962284502 小时前
Python 和Java 哪个更适合做自动化测试?
java·自动化测试·python·接口测试·性能测试
随遇而安zx2 小时前
【多线程】---AQS 原理 知识点(设计思想与源码深度解析)
java·多线程·aqs
LXMXHJ2 小时前
springboot中的线程操作
java·spring boot·后端·线程