本文正在参与「金石计划」

前言

参数验证很重要,是平常开发环节中不可少的一部分,可是我想很多后端搭档会偷懒,干脆不错,这样很或许给系统的稳定性和安全性带来严重的危害。那么在Spring Boot运用中怎么做好参数校验作业呢,本文供给了10个小技巧,你知道几个呢?

1.运用验证注解

Spring Boot 供给了内置的验证注解,能够协助简单、快速地对输入字段进行验证,例如查看 null 或空字段、强制执行长度限制、运用正则表达式验证形式以及验证电子邮件地址。

一些最常用的验证注释包含:

  • @NotNull:指定字段不能为空。
  • @NotEmpty:指定列表字段不能为空。
  • @NotBlank:指定字符串字段不得为空或仅包含空格。
  • @Min@Max:指定数字字段的最小值和最大值。
  • @Pattern:指定字符串字段有必要匹配的正则表达式形式。
  • @Email:指定字符串字段有必要是有用的电子邮件地址。

详细用法参考下面例子:

public class User {
    @NotNull
    private Long id;
    @NotBlank
    @Size(min = 2, max = 50)
    private String firstName;
    @NotBlank
    @Size(min = 2, max = 50)
    private String lastName;
    @Email
    private String email;
    @NotNull
    @Min(18)
    @Max(99)
    private Integer age;
    @NotEmpty
    private List<String> hobbies;
    @Pattern(regexp = "[A-Z]{2}\d{4}")
    private String employeeId;

2 运用自界说验证注解

尽管 Spring Boot 的内置验证注释很有用,但它们或许无法包含一切状况。假如有特别参数验证的场景,能够运用 Spring 的 JSR 303 验证结构创立自界说验证注释。自界说注解能够让你的的验证逻辑更具可重用性和可保护性。

假定咱们有一个运用程序,用户能够在其间创立帖子。每个帖子都应该有一个标题和一个正文,而且标题在一切帖子中应该是仅有的。尽管 Spring Boot 供给了用于查看字段是否为空的内置验证注释,但它没有供给用于查看仅有性的内置验证注释。在这种状况下,咱们能够创立一个自界说验证注解来处理这种状况。

首要,咱们创立自界说束缚注解UniqueTitle

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueTitleValidator.class)
public @interface UniqueTitle {
    String message() default "Title must be unique";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

接下来,咱们创立一个PostRepository接口,意图是从数据库中检索帖子:

public interface PostRepository extends JpaRepository<Post, Long> {
    Post findByTitle(String title);
}

然后咱们需求界说验证器类 UniqueTitleValidator,如下所示:

@Component
public class UniqueTitleValidator implements ConstraintValidator<UniqueTitle, String> {
    @Autowired
    private PostRepository postRepository;
    @Override
    public boolean isValid(String title, ConstraintValidatorContext context) {
        if (title == null) {
            return true;
        }
        return Objects.isNull(postRepository.findByTitle(title));
    }
}

UniqueTitleValidator实现了ConstraintValidator接口,它有两个泛型类型:第一个是自界说注解UniqueTitle,第二个是正在验证的字段类型(在本例中为String). 咱们还主动装配了PostRepository 类以从数据库中检索帖子。

isValid()办法经过查询 PostRepository 来查看 title 是否为 null 或许它是否是仅有的。假如 title 为 null 或仅有,则验证成功,并回来 true。

界说了自界说验证注释和验证器类后,咱们现在能够运用它来验证 Spring Boot 运用程序中的帖子标题:

public class Post {
    @UniqueTitle
    private String title;
    @NotNull
    private String body;
}

咱们已将 @UniqueTitle 注释运用于 Post 类中的 title 变量。验证此字段时,这将触发 UniqueTitleValidator 类中界说的验证逻辑。

3 在服务器端验证

除了前端或许客户端做了验证意外,服务器端验证输入是至关重要的。它能够保证在处理或存储任何歹意或格式过错的数据之前将其捕获,这关于运用程序的安全性和稳定性至关重要。

假定咱们有一个答运用户创立新帐户的 REST 端点。端点需求一个包含用户用户名和暗码的 JSON 恳求体。为保证输入有用,咱们能够创立一个 DTO(数据传输目标)类并将验证注释运用于其字段:

public class UserDTO {
    @NotBlank
    private String username;
    @NotBlank
    private String password;
}

咱们运用@NotBlank注解来保证usernamepassword字段不为空或 null。

接下来,咱们能够创立一个控制器办法来处理 HTTP POST 恳求并在创立新用户之前验证输入:

@RestController
@RequestMapping("/users")
@Validated
public class UserController {
    @Autowired
    private UserService userService;
    @PostMapping
    public ResponseEntity<String> createUser(@Valid @RequestBody UserDTO userDto) {
        userService.createUser(userDto);
        return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully");
    }
}

咱们运用 Spring 的@Validated注解来启用办法级验证,咱们还将 @Valid 注释运用于 userDto 参数以触发验证进程。

4 供给有意义的过错信息

当验证失利时,有必要供给清晰简练的过错音讯来描绘出了什么问题以及怎么修正它。

这是一个示例,假如咱们有一个答运用户创立新用户的 RESTful API。咱们要保证名字和电子邮件地址字段不为空,年纪在 18 到 99 岁之间,除了这些字段,假如用户测验运用重复的“用户名”创立帐户,咱们还会供给明确的过错音讯或“电子邮件”。

为此,咱们能够界说一个带有必要验证注释的模型类 User,如下所示:

public class User {
    @NotBlank(message = "用户名不能为空")
    private String name;
    @NotBlank(message = "Email不能为空")
    @Email(message = "无效的Emaild地址")
    private String email;
    @NotNull(message = "年纪不能为空")
    @Min(value = 18, message = "年纪有必要大于18")
    @Max(value = 99, message = "年纪有必要小于99")
    private Integer age;
}
  • 咱们运用 message属性为每个验证注释供给了自界说过错音讯。

接下来,在咱们的 Spring 控制器中,咱们能够处理表单提交并运用 @Valid 注释验证用户输入:

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;
    @PostMapping
    public ResponseEntity<String> createUser(@Valid @RequestBody User user, BindingResult result) {
        if (result.hasErrors()) {
            List<String> errorMessages = result.getAllErrors().stream()
                    .map(DefaultMessageSourceResolvable::getDefaultMessage)
                    .collect(Collectors.toList());
            return ResponseEntity.badRequest().body(errorMessages.toString());
        }
        // save the user to the database using UserService
        userService.saveUser(user);
        return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully");
    }
}
  • 咱们运用 @Valid 注释来触发 User 目标的验证,并运用 BindingResult 目标来捕获任何验证过错。

5 将 i18n 用于过错音讯

假如你的运用程序支持多种言语,则有必要运用国际化 (i18n) 以用户首选言语显现过错音讯。

以下是在 Spring Boot 运用程序中运用 i18n 处理过错音讯的示例

  1. 首要,在资源目录下创立一个包含默认过错音讯的 messages.properties 文件
# messages.properties
user.name.required=Name is required.
user.email.invalid=Invalid email format.
user.age.invalid=Age must be a number between 18 and 99.
  1. 接下来,为每种支持的言语创立一个 messages_xx.properties 文件,例如,中文的 messages_zh_CN.properties
user.name.required=称号不能为空.
user.email.invalid=无效的email格式.
user.age.invalid=年纪有必要在1899岁之间.
  1. 然后,更新您的验证注释以运用本地化的过错音讯
public class User {
    @NotNull(message = "{user.id.required}")
    private Long id;
    @NotBlank(message = "{user.name.required}")
    private String name;
    @Email(message = "{user.email.invalid}")
    private String email;
    @NotNull(message = "{user.age.required}")
    @Min(value = 18, message = "{user.age.invalid}")
    @Max(value = 99, message = "{user.age.invalid}")
    private Integer age;
}
  1. 最终,在 Spring 装备文件中装备 MessageSource bean 以加载 i18n 音讯文件
@Configuration
public class AppConfig {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }
    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalValidatorFactoryBean validatorFactoryBean = new LocalValidatorFactoryBean();
        validatorFactoryBean.setValidationMessageSource(messageSource());
        return validatorFactoryBean;
    }
}
  1. 现在,当发生验证过错时,过错音讯将依据随恳求发送的“Accept-Language”标头以用户的首选言语显现。

6 运用分组验证

验证组是 Spring Boot 验证结构的一个强壮功能,答应您依据其他输入值或运用程序状态运用条件验证规矩。

现在有一个包含三个字段的User类的状况下:firstNamelastNameemail。咱们要保证假如 email 字段为空,则 firstNamelastName 字段有必要非空。否则,一切三个字段都应该正常验证。

为此,咱们将界说两个验证组:EmailNotEmptyDefaultEmailNotEmpty 组将包含当 email 字段不为空时的验证规矩,而 Default 组将包含一切三个字段的正常验证规矩。

  1. 创立带有验证组的 User
public class User {
    @NotBlank(groups = Default.class)
    private String firstName;
    @NotBlank(groups = Default.class)
    private String lastName;
    @Email(groups = EmailNotEmpty.class)
    private String email;
    // getters and setters omitted for brevity
    public interface EmailNotEmpty {}
    public interface Default {}
}
  • 请注意,咱们在User类中界说了两个接口,EmailNotEmptyDefault。这些将作为咱们的验证组。
  1. 接下来,咱们更新Controller运用这些验证组
@RestController
@RequestMapping("/users")
@Validated
public class UserController {
    public ResponseEntity<String> createUser(
            @Validated({org.example.model.ex6.User.EmailNotEmpty.class}) @RequestBody User userWithEmail,
            @Validated({User.Default.class}) @RequestBody User userWithoutEmail)
    {
        // Create the user and return a success response
    }
}
  • 咱们已将@Validated注释添加到咱们的控制器,标明咱们想要运用验证组。咱们还更新了 createUser 办法,将两个 User 目标作为输入,一个在 email 字段不为空时运用,另一个在它为空时运用。
  • @Validated 注释用于指定将哪个验证组运用于每个 User 目标。关于 userWithEmail 参数,咱们指定了 EmailNotEmpty 组,而关于 userWithoutEmail 参数,咱们指定了 Default 组。
  1. 进行这些更改后,现在将依据“电子邮件”字段是否为空对“用户”类进行不同的验证。假如为空,则 firstNamelastName 字段有必要非空。否则,一切三个字段都将正常验证。

7 对杂乱逻辑运用跨域验证

假如需求验证跨多个字段的杂乱输入规矩,能够运用跨字段验证来保持验证逻辑的组织性和可保护性。跨字段验证可保证一切输入值均有用且彼此一致,然后避免出现意外行为。

假定咱们有一个表单,用户能够在其间输入任务的开端日期和结束日期,而且咱们期望保证结束日期不早于开端日期。咱们能够运用跨域验证来实现这一点。

  1. 首要,咱们界说一个自界说验证注解EndDateAfterStartDate
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EndDateAfterStartDateValidator.class)
public @interface EndDateAfterStartDate {
    String message() default "End date must be after start date";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  1. 然后,咱们创立验证器EndDateAfterStartDateValidator
public class EndDateAfterStartDateValidator implements ConstraintValidator<EndDateAfterStartDate, TaskForm> {
    @Override
    public boolean isValid(TaskForm taskForm, ConstraintValidatorContext context) {
        if (taskForm.getStartDate() == null || taskForm.getEndDate() == null) {
            return true;
        }
        return taskForm.getEndDate().isAfter(taskForm.getStartDate());
    }
}
  1. 最终,咱们将EndDateAfterStartDate注释运用于咱们的表单目标TaskForm
@EndDateAfterStartDate
public class TaskForm {
    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}

现在,当用户提交表单时,验证结构将主动查看结束日期是否晚于开端日期,假如不是,则供给有意义的过错音讯。

8 对验证过错运用反常处理

能够运用反常处理ExceptionHandler来统一捕获和处理验证过错。

以下是怎么在 Spring Boot 中运用反常处理来处理验证过错的示例:

@RestControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                                  HttpHeaders headers, HttpStatus status,
                                                                  WebRequest request) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", status.value());
        // Get all errors
        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(x -> x.getDefaultMessage())
                .collect(Collectors.toList());
        body.put("errors", errors);
        return new ResponseEntity<>(body, headers, status);
    }
}

在这里,咱们创立了一个用 @RestControllerAdvice 注解的 RestExceptionHandler 类来处理咱们的 REST API 抛出的反常。然后咱们创立一个用 @ExceptionHandler 注解的办法来处理在验证失利时抛出的 MethodArgumentNotValidException

在处理程序办法中,咱们创立了一个 Map 目标来保存过错呼应的详细信息,包含时刻戳、HTTP 状态代码和过错音讯列表。咱们运用 MethodArgumentNotValidException 目标的 getBindingResult() 办法获取一切验证过错并将它们添加到过错音讯列表中。

最终,咱们回来一个包含过错呼应详细信息的ResponseEntity目标,包含作为呼应主体的过错音讯列表、HTTP 标头和 HTTP 状态代码。

有了这个反常处理代码,咱们的 REST API 抛出的任何验证过错都将被捕获并以结构化和有意义的格式回来给用户,然后更简单理解和解决问题。

9 测验你的验证逻辑

需求为你的验证逻辑编写单元测验,以协助保证它正常作业。

@DataJpaTest
public class UserValidationTest {
    @Autowired
    private TestEntityManager entityManager;
    @Autowired
    private Validator validator;
    @Test
    public void testValidation() {
        User user = new User();
        user.setFirstName("John");
        user.setLastName("Doe");
        user.setEmail("invalid email");
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        assertEquals(1, violations.size());
        assertEquals("must be a well-formed email address", violations.iterator().next().getMessage());
    }
}

咱们运用 JUnit 5 编写一个测验来验证具有无效电子邮件地址的“用户”目标。然后咱们运用 Validator 接口来验证 User 目标并查看是否回来了预期的验证过错。

10 考虑客户端验证

客户端验证能够经过向用户供给即时反应并减少对服务器的恳求数量来改进用户体会。可是,不该依靠它作为验证输入的仅有办法。客户端验证很简单被绕过或操纵,因此有必要在服务器端验证输入,以保证安全性和数据完整性。

总结

有用的验证关于任何 Web 运用程序的稳定性和安全性都是必不可少的。Spring Boot 供给了一套东西和库来简化验证逻辑并使其更易于保护。经过遵从本文中讨论的最佳实践,您能够保证您的验证组件有用并供给超卓的用户体会。

欢迎重视个人大众号【JAVA旭阳】交流学习!