【问题标题】:Spring Boot : Custom Validation in Request ParamsSpring Boot:请求参数中的自定义验证
【发布时间】:2020-04-12 19:51:51
【问题描述】:

我想验证我的控制器中的请求参数之一。请求参数应来自给定值列表之一,如果不是,则应引发错误。在下面的代码中,我希望请求参数 orderBy 来自 @ValuesAllowed 中存在的值列表。

@RestController
@RequestMapping("/api/opportunity")
@Api(value = "Opportunity APIs")
@ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount",
        "ApplicationsApprovedCount" })
public class OpportunityController {

@GetMapping("/vendors/list")
    @ApiOperation(value = "Get all vendors")

    public ResultWrapperDTO getVendorpage(@RequestParam(required = false) String term,
            @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer size,
            @RequestParam(required = false) String orderBy, @RequestParam(required = false) String sortDir) {

我编写了一个自定义 bean 验证器,但不知何故这不起作用。即使为查询参数传递任何随机值,它也不会验证并引发错误。

@Repeatable(ValuesAllowedMultiple.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {ValuesAllowedValidator.class})
public @interface ValuesAllowed {

    String message() default "Field value should be from list of ";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String propName();
    String[] values();
}
public class ValuesAllowedValidator implements ConstraintValidator<ValuesAllowed, Object> {

    private String propName;
    private String message;
    private String[] values;

    @Override
    public void initialize(ValuesAllowed requiredIfChecked) {
        propName = requiredIfChecked.propName();
        message = requiredIfChecked.message();
        values = requiredIfChecked.values();
    }

    @Override
    public boolean isValid(Object object, ConstraintValidatorContext context) {
        Boolean valid = true;
        try {
            Object checkedValue = BeanUtils.getProperty(object, propName);

            if (checkedValue != null) {
                valid = Arrays.asList(values).contains(checkedValue.toString().toLowerCase());
            } 

            if (!valid) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(message.concat(Arrays.toString(values)))
                        .addPropertyNode(propName).addConstraintViolation();
            }
        } catch (IllegalAccessException e) {
            log.error("Accessor method is not available for class : {}, exception : {}", object.getClass().getName(), e);
            return false;
        } catch (NoSuchMethodException e) {
            log.error("Field or method is not present on class : {}, exception : {}", object.getClass().getName(), e);
            return false;
        } catch (InvocationTargetException e) {
            log.error("An exception occurred while accessing class : {}, exception : {}", object.getClass().getName(), e);
            return false;
        }
        return valid;
    }
}

【问题讨论】:

    标签: spring-boot validation controller spring-annotations http-request-parameters


    【解决方案1】:

    案例1:如果ValuesAllowed注解根本没有被触发,可能是因为控制器没有使用@Validated注解。

    @Validated
    @ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount", "ApplicationsApprovedCount" })
    public class OpportunityController {
    @GetMapping("/vendors/list")
    public String getVendorpage(@RequestParam(required = false) String term,..{
    }
    

    情况2:如果触发并抛出错误,可能是因为BeanUtils.getProperty没有解析属性并抛出异常。

    如果上述解决方案不起作用,您可以尝试将注解移至方法级别并更新 Validator 以使用 OrderBy 参数的有效值列表。这对我有用。下面是示例代码。

    @RestController
    @RequestMapping("/api/opportunity")
    @Validated
    public class OpportunityController {
        @GetMapping("/vendors/list")
        public String getVendorpage(@RequestParam(required = false) String term,
                @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer size,
                @ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount",
                        "ApplicationsApprovedCount" }) @RequestParam(required = false) String orderBy, @RequestParam(required = false) String sortDir) {
            return "success";
        }
    
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = { ValuesAllowed.Validator.class })
    public @interface ValuesAllowed {
    
        String message() default "Field value should be from list of ";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        String propName();
    
        String[] values();
    
        class Validator implements ConstraintValidator<ValuesAllowed, String> {
            private String propName;
            private String message;
            private List<String> allowable;
    
            @Override
            public void initialize(ValuesAllowed requiredIfChecked) {
                this.propName = requiredIfChecked.propName();
                this.message = requiredIfChecked.message();
                this.allowable = Arrays.asList(requiredIfChecked.values());
            }
    
            public boolean isValid(String value, ConstraintValidatorContext context) {
                Boolean valid = value == null || this.allowable.contains(value);
    
                if (!valid) {
                    context.disableDefaultConstraintViolation();
                    context.buildConstraintViolationWithTemplate(message.concat(this.allowable.toString()))
                            .addPropertyNode(this.propName).addConstraintViolation();
                }
                return valid;
            }
        }
    }
    

    【讨论】:

    • 您的代码运行良好。但是如何使这个 ValuesAllowed 验证器可重用,现在允许值的数组在类中是硬编码的,您能否编辑您的答案以使代码可重用于验证任何字段。
    • 我已经编辑了答案以通过控制器传递值。通常,如果您使用的是 Request Bean,您可以从 Controller 传递值列表或注释字段。
    • 添加valuesallowed注解后,request param变成了必填参数,如何让它成为可选参数。
    • AFAIK,@RequestParamValidator 在这里分别工作。为了使它成为可选的,当我们为值传递 null 时,我们可以在验证器中返回 true。代码 - Boolean valid = value == null || this.allowable.contains(value); 通过这样做,当我们不传递值时,它不会抛出异常。
    • @lavanyap 在我的控制器中我正在使用@Validated 仍然没有触发我的注释。你能帮忙吗?我正在验证multipart 对象
    【解决方案2】:

    您必须更改一些内容才能使此验证生效。

    控制器应该用@Validated注释,@ValuesAllowed应该在方法中注释目标参数。

    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    @Validated
    @RestController
    @RequestMapping("/api/opportunity")
    public class OpportunityController {
    
        @GetMapping("/vendors/list")
        public String getVendorpage(
                @RequestParam(required = false)
                @ValuesAllowed(values = {
                        "OpportunityCount",
                        "OpportunityPublishedCount",
                        "ApplicationCount",
                        "ApplicationsApprovedCount"
                }) String orderBy,
                @RequestParam(required = false) String term,
                @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer size,
                @RequestParam(required = false) String sortDir) {
            return "OK";
        }
    }
    

    @ValuesAllowed 应该以 ElementType.PARAMETER 为目标,在这种情况下,您不再需要 propName 属性,因为 Spring 将验证所需的参数。

    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target({ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = {ValuesAllowedValidator.class})
    public @interface ValuesAllowed {
    
        String message() default "Field value should be from list of ";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    
        String[] values();
    }
    

    验证器:

    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    import java.util.Arrays;
    import java.util.List;
    
    public class ValuesAllowedValidator implements ConstraintValidator<ValuesAllowed, String> {
    
        private List<String> expectedValues;
        private String returnMessage;
    
        @Override
        public void initialize(ValuesAllowed requiredIfChecked) {
            expectedValues = Arrays.asList(requiredIfChecked.values());
            returnMessage = requiredIfChecked.message().concat(expectedValues.toString());
        }
    
        @Override
        public boolean isValid(String testValue, ConstraintValidatorContext context) {
            boolean valid = expectedValues.contains(testValue);
    
            if (!valid) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(returnMessage)
                        .addConstraintViolation();
            }
            return valid;
        }
    }
    

    但上面的代码返回 HTTP 500 并使用丑陋的堆栈跟踪污染日志。为避免这种情况,您可以将这样的 @ExceptionHandler 方法放在控制器主体中(因此它的作用域仅适用于该控制器)并且您可以控制 HTTP 状态:

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    String handleConstraintViolationException(ConstraintViolationException e) {
        return "Validation error: " + e.getMessage();
    }
    

    ...或者您可以将此方法放在单独的@ControllerAdvice 类中,并对该验证具有更多控制权,例如在所有控制器或仅所需的控制器上使用它。

    【讨论】:

    • 我如何确保 ValuesAllowed 可以使用 equalsIgnorecase 以防客户想要在所有 Caps 或 CamelCase 中传递值?
    • @TarunPande in ValuesAllowedValidator#initialize 您需要将每个 requiredIfChecked.values() 映射为小写,在检查列表内容时在 ValuesAllowedValidator#isValid 中执行相同操作。我还将布尔 ignoreCase 字段添加到 ValuesAllowed 接口,这样就很清楚了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-05
    • 1970-01-01
    相关资源
    最近更新 更多