Custom annotations to achieve parameter verification

Personal blog address:
http://xiaohe-blog.top/

1. Why parameter verification is required

When working on the backend, we need to receive data from the frontend to query the database, but if some data is too outrageous, we can pass it directly to prevent such junk data from touching the database and reduce the pressure on the database.

Sometimes there will be unscrupulous people attacking our program through some junk data, causing our server or database to crash. Although this kind of attack is low-level, it has to be prevented, just like when QQ makes a login request, they send the account = 123, the data of password = 123 is sent 10,000 times per second, which is obviously looking for trouble. Well, why can human hands reach 10,000 times per second?

The solution is: on the one hand, we can reject some requests by recording the ip/account in Redis. For example, the same ip/account can request up to 100 times in 1s. On the other hand, it is to perform data verification to pass part of the data. How many times in these 100 times are garbage data. This minimizes the pressure on the server database.

2. How to implement parameter verification

To be honest, there are quite a lot of ways to realize parameter verification. Personally, I have used it to write directly in the Controller code, AOP+custom annotation, ConstraintValidator. This blog is about ConstraintValidatorimplementation.

Write directly in the Controller code. To be honest, it is simple to write, but it is bloated, highly coupled, and most importantly, not elegant enough.

AOP implementation is difficult, the code is cumbersome, and the logic is messy.

So I recommend using ConstraintValidator.

Here we first provide a tool class for parameter verification, providing verification methods for mobile phone number, email, verification code, password, and ID number, which can be used directly by copying. When I perform parameter verification later, I use the verification method in this class.

/**
 * @description : 验证手机号、身份证号、密码、验证码、邮箱的工具类
 * @author : 小何
 */
public class VerifyUtils {
    
    
    /**
     * 手机号正则
     */
    public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";

    /**
     * 邮箱正则
     */
    public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";

    /**
     * 密码正则。4~32位的字母、数字、下划线
     */
    public static final String PASSWORD_REGEX = "^\\w{4,32}$";

    /**
     * 验证码正则, 6位数字或字母
     */
    public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";

    /**
     * 身份证号正则
     */
    public static final String ID_CARD_NUMBER_REGEX_18 = "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";
    public static final String ID_CARD_NUMBER_REGEX_15 = "^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{2}$";


    /**
     * 手机号是否合法
     * @param phone 要校验的手机号
     * @return true:符合,false:不符合
     */
    public static boolean isPhoneLegal(String phone){
    
    
        return match(phone, PHONE_REGEX);
    }
    /**
     * 是否是无效邮箱格式
     * @param email 要校验的邮箱
     * @return true:符合,false:不符合
     */
    public static boolean isEmailLegal(String email){
    
    
        return match(email, EMAIL_REGEX);
    }

    /**
     * 是否是无效验证码格式
     * @param code 要校验的验证码
     * @return true:符合,false:不符合
     */
    public static boolean isCodeLegal(String code){
    
    
        return match(code, VERIFY_CODE_REGEX);
    }

    // 校验是否不符合正则格式
    private static boolean match(String str, String regex){
    
    
        if (str == null || "".equals(str)) {
    
    
            return false;
        }
        return str.matches(regex);
    }

    /**
     * 验证身份证号是否合法
     * @param idCard 身份证号
     * @return true: 合法;    false:不合法
     */
    public static boolean isIdCardLegal(String idCard) {
    
    
        if (idCard.length() == 18) {
    
    
            return match(idCard, ID_CARD_NUMBER_REGEX_18);
        } else {
    
    
            return match(idCard, ID_CARD_NUMBER_REGEX_15);
        }
    }
}

Use Cases:

public static void main(String[] args) {
    
    
    String phone = "15039469595";
    boolean phoneLegal = VerifyUtils.isPhoneLegal(phone);
    System.out.println(phoneLegal);
}

3. Annotation to achieve parameter verification

First import dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

After importing dependencies, you can try to use its own parameter verification annotation: @NotNull non-null verification

First, let’s talk about the steps to use annotations to implement parameter verification.

In the demos I usually write, I prefer to define vo for the interface to receive data. For example, the data transmitted by the front end is the username and password in the user object. There are many fields in our user. It would be too wasteful to simply use user. And if you directly make custom annotations on the entity class, it will cause code pollution to the entity class. So I personally think that it is necessary to define the vo class.

The following is my login interface:

@PostMapping("/login")
public String login(@RequestBody @Validated LoginVo user) {
    
    

    return "user:" + user.toString();
}

The following is the vo class of my login interface:

@Data
public class LoginVo {
    
    
    // 邮箱
    @NotNull(message = "邮箱不能为空")
    private String email;
    // 密码
    private String password;
}

You may have noticed that I wrote two more annotations: @Validated@NotNull(message = "Email cannot be empty")

Yes, using annotations for parameter verification is divided into two steps:

  1. Add the corresponding verification method to the field that needs to be verified, such as @NotNull
  2. Add @Validated before the interface parameters that need to be verified, and tell Spring that you can show me this class. Some fields in it have added verification annotations. If the requirements are met, it will be released, and if the requirements are not met, an error will be reported.

as the picture shows:

image-20221218234006905

image-20221218234041145

Use postman to initiate a request, deliberately making the mailbox empty:

image-20221218234216085

You will find an error:

Resolved [org.springframework.web.bind.MethodArgumentNotValidException: 
Validation failed for argument [0] in public java.lang.String com.example.demo.controller.UserController.login(com.example.demo.domain.vo.LoginVo): 
[Field error in object 'loginVo' on field 'email': rejected value [null]; codes [NotNull.loginVo.email,NotNull.email,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [loginVo.email,email]; arguments []; 
default message [email]]; 
default message [邮箱不能为空]] ]

When this exception occurs: MethodArgumentNotValidException, we can catch it in the global exception handler and return a more standardized information.

4. Custom annotations to achieve parameter verification

After learning how to use annotations for parameter verification, we can proceed to the next work: custom annotations.

Due to the complexity of the requirements, we now need to complete the registration interface. When registering, we need ID number, phone number, email address, and password. The annotation verification of these fields is not implemented by Spring for us. At this time, DIY annotations are needed to meet the requirements.

How to implement custom annotations? Let's imitate first, let's take a look at what's in the @NotNull annotation:

image-20221218234955678

Commonly used annotations such as @Target, @Retention, @Repeatable, @Documented will not be explained anymore.

@Constraint: Indicates that this annotation is an annotation for parameter validation. validateBy specifies the implementation class of the validation rule, and the implementation class.class needs to be filled here.

The meaning of each field:

message : The error message after the data does not meet the verification rules. It can be a string or a file. If there are many verification fields, it is recommended to implement the file format.

groups : Specify annotation usage scenarios, such as adding and deleting

payload: often used for Bean

The above three fields are all required, and each parameter verification using ConstraintValidator must have these three fields.

The latter List is exclusive to NotNull, so don't care.

Then we can imitate @NotNull to implement custom annotations.

The first step: implement the verification class:

An interface needs to be implemented: ConstraintValidator<?, ?>

# ConstraintValidator<?, ?>
第一个参数是自定义注解
第二个参数是需要进行校验的数据的数据类型
例如想对手机号校验,第一个参数是Phone,第二个参数是String

This interface provides a method:

boolean isValid(T value, ConstraintValidatorContext context);

The first parameter is the data from the front end. We can judge this data and return a Boolean value

public class VerifyPhone implements ConstraintValidator<Phone, String> {
    
    

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
    
    
       // 判断手机号是否合法
        return VerifyUtils.isPhoneLegal(s);
    }
}

Step 2: Implement the annotation. The name of this annotation needs to be consistent with the first parameter of ConstraintValidator.

In particular, the value of validatedBy in the @Constraint annotation is the Class instance of the first step.

@Target({
    
    ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
        validatedBy = {
    
    VerifyPhone.class}
)
public @interface Phone {
    
    
    boolean isRequired() default false;

    String message() default "手机号格式错误";

    Class<?>[] groups() default {
    
    };

    Class<? extends Payload>[] payload() default {
    
    };

}

Step 3: Add corresponding annotations to the fields.

@Data
public class RegisterVo {
    
    
    private String name;
    // 身份证号
    private String id;
    // 电话号码
    @Phone
    private String phone;
    // 邮箱
    private String email;
    // 密码
    private String password;
}

Step 4: Add before the parameter @Validated.

@PutMapping("/register")
public String register(@RequestBody @Validated RegisterVo user) {
    
    
    return "user: " + user.toString();
}

In this way, parameter verification is elegantly implemented. Don't think it's troublesome for us to make so many classes, unless you want to write like this in every controller:

@PutMapping("/register")
public String register(@RequestBody @Validated RegisterVo user) {
    
    
    if (VerifyUtils.isPhoneLegal("xxx")) {
    
    
        return "手机号格式错误";
    }
    if (VerifyUtils.isCodeLegal("xxx")) {
    
    
        return "验证码格式错误";
    }
    if (VerifyUtils.isIdCardLegal("xxx")) {
    
    
        return "身份证格式错误";
    }
    if (VerifyUtils.isEmailLegal("xxx")) {
    
    
        return "邮箱格式错误";
    }
    return "user: " + user.toString();
}

It's really low and troublesome, okay?

Maybe the steps are a bit cumbersome, but there are only 4 steps. Draw a picture to strengthen your memory:

image-20221219004110257

Guess you like

Origin blog.csdn.net/qq_62939743/article/details/128367789