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

概述

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

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

纲要

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

Passay:专业密码策略库

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

引入依赖

在 pom.xml 中添加 passay 和 spring-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 {};
}

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

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,有效提高代码可读性与安全性。

相关推荐
自强的小白2 分钟前
Spring事务失效的场景
java·spring
JosieBook3 分钟前
【WinForm 代码反脆弱系列】04 数据库操作 —— 连接字符串、连接对象与连接池
数据库·oracle
小蒜学长9 分钟前
基于SpringBoot+Vue的小学数学智能出题系统(代码+数据库+LW)
java·数据库·spring boot·后端·智能出题系统
不灭的黄金瞳12314 分钟前
Java继承与多态
java·intellij-idea
71-316 分钟前
MySQL密码重置
数据库·笔记·学习·mysql
企业数字化笔记30 分钟前
视频字幕如何烧录到成片?硬字幕、软字幕、样式控制与播放验收
java·ffmpeg
小朱爱编程12342 分钟前
我用 Jev 做了三个实用工具:整理标签页、分诊飞书反馈、找回 GitHub 收藏
java·开发语言·人工智能·后端·python·架构·ai编程
锅里游的鱼吖1 小时前
echarts自定义折线图
前端·javascript·数据库
酷虎软件1 小时前
视频加标题字幕 API 接口文档
java·数据库·mysql
谢亮_vipxieliang1 小时前
Java 8/11/17/21/25 怎么选?一篇讲清 LTS 升级路线
java·开发语言