在业务的实现过程中，尤其是对外接口开发，我们需要对请求进行大量的验证并返回错误状态码和描述。在前面的内容中也已经使用过验证机制。


该文将介绍 [solon-security-validation](/article/225) 插件的使用和扩展。能力实现与说明：


| 加注位置 | 能力实现基础 | 说明 |
| -------- | -------- | -------- |
| 函数     | 基于 `@Addition(Filter.class)` 实现     | 对请求上下文做校验（属于注入前校验）     |
| 参数     | 基于 `@Around(Interceptor.class)` 实现     | 对函数的参数值做校验（属于注入后校验）     |


使用效果如下：

```java
//可以加在方法上、或控制器类上（或者控制器基类上）
@Valid
@Controller
public class UserController {
    //
    //这里只是演示，用时别乱加
    //
    @NoRepeatSubmit  //重复提交验证（加上方法上的，为注入之前校验）
    @Whitelist     //白名单验证（加上方法上的，为注入之前校验）
    @Mapping("/user/add")
    public void addUser(
            @NotNull String name, 
            @Pattern("^http") String icon, //注解在参数或字段上时，不需要加 value 属性
            @Validated User user) //实体校验，需要加 @Validated
    { 
        //...
    }
    
    //分组校验
    @Mapping("/user/update")
    public void updateUser(@Validated(UpdateLabel.class) User user){
        //...
    }
}

@Data
public class User {
    @NotNull(groups = UpdateLabel.class) //用于分组校验
    private Long id;
    
    @NotNull
    private String nickname;
    
    @Email  //注解在参数或字段上时，不需要加 value 属性
    private String email;
    
    @Validated //验证列表里的实体
    @NotNull
    @Size(min=1) //最少要有1个
    private List<Order> orderList;
}
```

也可用于组件类上（可以是非 web 项目）

```java
//可以加在方法上、或组件类上（或者基类上）
@Valid
@Component
public class UserService {
    public void addUser(
            @NotNull String name, 
            @Pattern("^http") String icon, //注解在参数或字段上时，不需要加 value 属性
            @Validated User user) //实体校验，需要加 @Validated
    { 
        //...
    }
    
    //分组校验
    public void updateUser(@Validated(UpdateLabel.class) User user){
        //...
    }
}
```

也支持工具手动校验（放哪儿都方便）

```java
User user = new User();
ValidUtils.validateEntity(user);
```


默认策略，有校验不通过的会马上返回。如果校验所有，需加配置声明（返回的信息结构会略不同）：

```yaml
solon.validation.validateAll: true
```



Solon 的校验框架，可支持Context的参数较验（即请求传入的参数），也可支持实体字段较验。



| 注解  | 作用范围 |  说明 | 
| -------- | -------- | -------- | 
| Valid | 控制器类 | 启用校验能力（加在控制器类上，或者控制器基类上） |
|   | | |
| Validated | 参数 或 字段 | 校验（参数或字段的类型）实体（或实体集合）上的字段     | 
|   | | |
| Date    | 参数 或 字段 | 校验注解的值为日期格式    | 
| DecimalMax(value)    | 参数 或 字段 | 校验注解的值小于等于@ DecimalMax指定的value值     | 
| DecimalMin(value)     | 参数 或 字段 | 校验注解的值大于等于@ DecimalMin指定的value值     | 
| Email    | 参数 或 字段 | 校验注解的值为电子邮箱格式    | 
| Length(min, max)    | 参数 或 字段 | 校验注解的值长度在min和max区间内（对字符串有效）     | 
| Logined    |  控制器 或 动作 | 校验本次请求主体已登录     | 
| Max(value)    |  参数 或 字段 | 校验注解的值小于等于@Max指定的value值     | 
| Min(value)     | 参数 或 字段 | 校验注解的值大于等于@Min指定的value值     | 
| NoRepeatSubmit    | 控制器 或 动作  | 校验本次请求没有重复提交     | 
| NotBlacklist    | 控制器 或 动作 | 校验本次请求主体不在黑名单     | 
| NotBlank    | 动作 或 参数 或 字段 | 校验注解的值不是空白（for String）     | 
| NotEmpty    | 动作 或 参数 或 字段 | 校验注解的值不是空（for String）     | 
| NotNull   | 动作 或 参数 或 字段 | 校验注解的值不是null     | 
| NotZero  | 动作 或 参数 或 字段 | 校验注解的值不是0     | 
| Null    | 动作 或 参数 或 字段 | 校验注解的值是null     | 
| Numeric    | 动作 或 参数 或 字段 | 校验注解的值为数字格式    | 
| Pattern(value)    | 参数 或 字段 | 校验注解的值与指定的正则表达式匹配    | 
| Size   | 参数 或 字段 | 校验注解的集合大小在min和max区间内（对集合有效）    | 
| Whitelist    | 控制器 或 动作 | 校验本次请求主体在白名单范围内     | 


注1：可作用在 [动作 或 参数] 上的注解，加在动作上时可支持多个参数的校验。

注2：如果 json body 提交的数据，想在 [动作] 上验证，可通过 过滤器 把 json 数据转换部分到 ctx.paramMap()。


### 1、关于 Context 的校验（即注入前校验）

加在控制器方法上的校验，为 Context 的较验（如 Header，Param，Cookie，Body，IP 等...）。可以做格式方面的校验（比如确保某参数是数字格式），还可以做限制性的校验（比如鉴权，比如白名单，比如流量限制等）。


```java
@Valid
@Controller
public class UserController {
    @NoRepeatSubmit  //重复提交验证（加上方法上的，为注入之前校验）
    @Whitelist     //白名单验证（加上方法上的，为注入之前校验）
    @NotNull({"name","type"}) //非Null验证（加上方法上的，为注入之前校验）
    @Mapping("/user/add")
    public void addUser(@Validated User user) //实体校验，需要加 @Validated
    { 
        //...
    }
}
```


### 2、开始定制使用

solon-validation 通过 ValidatorManager，提供了一组定制和扩展接口。

#### @NoRepeatSubmit 改为分布式锁验证

NoRepeatSubmit 默认使用了本地延时锁。如果是分布式环境，需要定制为分布式锁：

```java
@Component
public class NoRepeatSubmitCheckerNew implements NoRepeatSubmitChecker {
    @Override
    public boolean check(NoRepeatSubmit anno, Context ctx, String submitHash, int limitSeconds) {
        return LockUtils.tryLock(Solon.cfg().appName(), submitHash, limitSeconds);
    }
}

//或者去掉 @Component 手动注册到 ValidatorManager
//ValidatorManager.setNoRepeatSubmitChecker(new NoRepeatSubmitCheckerNew());
```

或者 完全重写 NoRepeatSubmitValidator，并进行重新注册

#### @Whitelist 实现验证

框架层面没办法为 Whitelist 提供一个名单库，所以需要通过一个接口实现完成对接。

```java
@Component
public class WhitelistCheckerNew implements WhitelistChecker {
    @Override
    public boolean check(Whitelist anno, Context ctx) {
        String ip = ctx.realIp();

        return CloudClient.list().inListOfServerIp(ip);
    }
}

//或者去掉 @Component 手动注册到 ValidatorManager
//ValidatorManager.setWhitelistChecker(new WhitelistCheckerNew());
```

或者 完全重写 WhitelistValidator，并进行重新注册


### 3、校验异常处理

#### 通过过滤器（或，路由拦截器）捕捉异常

```java
//可以和其它异常处理合并一个过滤器
@Component
public class ValidatorFailureFilter implements Filter {
    @Override
    public void doFilter(Context ctx, FilterChain chain) throws Throwable {
        try {
            chain.doFilter(ctx);
        } catch (ValidatorException e) {
            //v1.10.4 后，添加 getCode() 接口
            ctx.render(Result.failure(e.getCode(), e.getMessage()));
        }
    }
}
```

#### 定制 ValidatorFailureHandler 接口的组件，构建提示信息（可选，一般默认的就够了）

```java
//通过定义 ValidatorFailureHandler 实现类的组件，实现自动注册。
@Component
public class ValidatorFailureHandlerImpl implements ValidatorFailureHandler {
    @Override
    public boolean onFailure(Context ctx, Annotation anno, Result rst, String message) throws Throwable {
        //可以对 message 作国际化转换（校验注解可以配置消息的 code，此处统一转换）
        
        if (Utils.isEmpty(message)) {
            if (Utils.isEmpty(rst.getDescription())) {
                message = new StringBuilder(100)
                        .append("@")
                        .append(anno.annotationType().getSimpleName())
                        .append(" verification failed")
                        .toString();
            } else {
                message = new StringBuilder(100)
                        .append("@")
                        .append(anno.annotationType().getSimpleName())
                        .append(" verification failed: ")
                        .append(rst.getDescription())
                        .toString();
            }
        }
        //这里也可以直接做输出，不过用异常更好
        throw new ValidatorException(rst.getCode(), message, anno, rst);
    }
}

//也可以手动配置（找个地方写一下）
//ValidatorManager.setFailureHandler((ctx, ano, rst, message) -> {
//    //..
//});
```


### 4、尝试添一个扩展校验注解

#### 先定义个校验注解 @Date 

偷懒一下，直接把自带的扔出来了。只要看这过程后，能自己搞就行了:-P

```java
@Target({ElementType.PARAMETER, ElementType.FIELD}) 
@Retention(RetentionPolicy.RUNTIME)
public @interface Date {
    @Note("日期表达式, 默认为：ISO格式")
    String value() default  "";

     /**
     * 提示消息
     * */
    String message() default "";

    /**
     * 校验分组
     * */
    Class<?>[] groups() default {};
}
```

#### 添加 @Date 的校验器实现类

```java
public class DateValidator implements Validator<Date> {
    public static final DateValidator instance = new DateValidator();


    @Override
    public String message(Date anno) {
        return anno.message();
    }

    @Override
    public Class<?>[] groups(Date anno) {
        return anno.groups();
    }

    /**
     * 校验实体的字段（注解在参数或字段上有效，即注入后的校验）
     * */
    @Override
    public Result validateOfValue(Date anno, Object val0, StringBuilder tmp) {
        if (val0 != null && val0 instanceof String == false) {
            return Result.failure();
        }

        String val = (String) val0;

        if (verify(anno, val) == false) {
            return Result.failure();
        } else {
            return Result.succeed();
        }
    }

    /**
     * 校验上下文的参数（注解在方法上有效，即注入前的校验）
     * */
    @Override
    public Result validateOfContext(Context ctx, Date anno, String name, StringBuilder tmp) {
        String val = ctx.param(name);

        if (verify(anno, val) == false) {
            return Result.failure(name);
        } else {
            return Result.succeed();
        }
    }

    private boolean verify(Date anno, String val) {
        //如果为空，算通过（交由 @NotNull 或 @NotEmpty 或 @NotBlank 进一步控制）
        if (Utils.isEmpty(val)) {
            return true;
        }

        try {
            if (Utils.isEmpty(anno.value())) {
                DateTimeFormatter.ISO_LOCAL_DATE_TIME.parse(val);
            } else {
                DateTimeFormatter.ofPattern(anno.value()).parse(val);
            }

            return true;
        } catch (Exception ex) {
            return false;
        }
    }
}
```

#### 注册到校验管理器

```java
@Configuration
public class Config {
    @Bean
    public void adapter() {
        //
        // 此处为注册验证器。如果有些验证器重写了，也是在此处注册
        //
        ValidatorManager.register(Date.class, new DateValidator());
    }
}
```

#### 可以使用它了

```java
@Valid
@Controller
public class UserController extends VerifyController{
    @Mapping("/user/add")
    public void addUser(String name, @Date("yyyy-MM-dd") String birthday){
        //...
    }
}
```









