F、请求参数校验、及定制与扩展
在业务的实现过程中,尤其是对外接口开发,我们需要对请求进行大量的验证并返回错误状态码和描述。在前面的内容中也已经使用过验证机制。
该文将介绍 solon-security-validation 插件的使用和扩展。能力实现与说明:
加注位置 | 能力实现基础 | 说明 |
---|---|---|
函数 | 基于 @Addition(Filter.class) 实现 | 对请求上下文里的值做校验(header, param..) |
参数 | 基于 @Around(Interceptor.class) 实现 | 对函数的参数值本身做校验 |
使用效果如下:
//可以加在方法上、或控制器类上(或者控制器基类上)
@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;
}
也可用于组件类上
//可以加在方法上、或组件类上(或者基类上)
@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){
//...
}
}
默认策略,有校验不通过的会马上返回。如果校验所有,需加配置申明(返回的信息结构会略不同):
solon.validation.validateAll: true
Solon 的校验框架,可支持Context的参数较验(即请求传入的参数),也可支持实体字段较验。
注解 | 作用范围 | 说明 |
---|---|---|
Valid | 控制器类 | 启用校验能力(加在控制器类上,或者控制器基类上) |
Validated | 参数 或 字段 | 校验(参数或字段的类型)实体(或实体集合)上的字段 |
Date | 参数 或 字段 | 校验注解的值为日期格式 |
DecimalMax(value) | 参数 或 字段 | 校验注解的值小于等于@ DecimalMax指定的value值 |
DecimalMin(value) | 参数 或 字段 | 校验注解的值大于等于@ DecimalMin指定的value值 |
参数 或 字段 | 校验注解的值为电子邮箱格式 | |
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、开始定制使用
solon-validation 通过 ValidatorManager,提供了一组定制和扩展接口。
@NoRepeatSubmit 改为分布式锁验证
NoRepeatSubmit 默认使用了本地延时锁。如果是分布式环境,需要定制为分布式锁:
@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 提供一个名单库,所以需要通过一个接口实现完成对接。
@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,并进行重新注册
2、校验异常处理
通过过滤器(或,路由拦截器)捕捉异常
//可以和其它异常处理合并一个过滤器
@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 接口的组件,构建提示信息(可选,一般默认的就够了)
//通过定义 ValidatorFailureHandler 实现类的组件,实现自动注册。
@Component
public class ValidatorFailureHandlerImpl implements ValidatorFailureHandler {
@Override
public boolean onFailure(Context ctx, Annotation anno, Result rst, String message) throws Throwable {
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) -> {
// //..
//});
3、尝试添一个扩展校验注解
先定义个校验注解 @Date
偷懒一下,直接把自带的扔出来了。只要看这过程后,能自己搞就行了:-P
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Date {
@Note("日期表达式, 默认为:ISO格式")
String value() default "";
String message() default "";
/**
* 校验分组
* */
Class<?>[] groups() default {};
}
添加 @Date 的校验器实现类
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;
}
}
}
注册到校验管理器
@Configuration
public class Config {
@Bean
public void adapter() {
//
// 此处为注册验证器。如果有些验证器重写了,也是在此处注册
//
ValidatorManager.register(Date.class, new DateValidator());
}
}
可以使用它了
@Valid
@Controller
public class UserController extends VerifyController{
@Mapping("/user/add")
public void addUser(String name, @Date("yyyy-MM-dd") String birthday){
//...
}
}