SpringBoot如何使用RequestBodyAdvice进行统一参数处理

  • Post category:http

关于SpringBoot如何使用RequestBodyAdvice进行统一参数处理,我们可以按照以下步骤来实现:

1.新建一个类,实现RequestBodyAdvice接口

在这个类中,我们可以定义在请求体内任意对象被反序列化之前需要执行的操作,比如参数加密或解密、参数校验等。

@ControllerAdvice
public class RequestBodyDecryptAdvice implements RequestBodyAdvice {

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        // 这里我们只做对象类型的判断
        return Object.class.equals(type);
    }

    @Override
    public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return o;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        return httpInputMessage;
    }

    @Override
    public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        // 在这里进行参数解密或校验等操作
        return o;
    }
}

可以看到,这里我们主要重写了RequestBodyAdvice接口中的四个方法:

  • boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass):该方法判断被注解的方法需要处理哪些对象类型的RequestBody参数。如果该方法返回true,则该RequestBody参数的处理交给下面其他三个方法。

  • HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<?> aClass):该方法在被注解的方法中的RequestBody参数被反序列化前执行,在这里可以修改请求报文。

  • Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<?> aClass):该方法在RequestBody参数被反序列化后,但在被注解的方法执行之前执行。

  • Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType):该方法在请求体为空时触发,一般不做处理。

2.在ControllerAdvice中添加@Order注解

如果我们的项目中同时使用了多个RequestBodyAdvice,那么每个RequestBodyAdvice都需要通过@Order注解标注顺序,否则可能会引起处理冲突。

@ControllerAdvice
@Order(1)
public class RequestBodyAdvice1 implements RequestBodyAdvice {

    //省略代码...
}

@ControllerAdvice
@Order(2)
public class RequestBodyAdvice2 implements RequestBodyAdvice {

    //省略代码...
}

3.在被注解的方法上添加@RequestBody注解

@PostMapping("/user")
public User addUser(@RequestBody User user) {
    return userService.addUser(user);
}

示例一:RequestBody参数加密

这里我们通过AES算法来对RequestBody进行加密处理:

@ControllerAdvice
public class RequestBodyEncryptAdvice implements RequestBodyAdvice {
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 这里我们只做对象类型的判断
        return Object.class.equals(targetType);
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        byte[] body = IOUtils.readFully(inputMessage.getBody(), inputMessage.getHeaders().getContentLength());
        byte[] decryptedBody = aesDecrypt(body, "password");
        return new DecryptedHttpInputMessage(inputMessage.getHeaders(), decryptedBody);
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    /**
     * 对body体进行aes解密
     * @param encrypted 加密后的body体
     * @param password 密钥
     */
    private static byte[] aesDecrypt(byte[] encrypted, String password) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(md5(password), "AES");
            Cipher cipher = Cipher.getInstance("AES");
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] decrypted = cipher.doFinal(encrypted);
            return decrypted;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 使用md5算法对密码进行处理,生成16字节的密钥
     */
    private static byte[] md5(String password) throws Exception {
        MessageDigest md = MessageDigest.getInstance("md5");
        byte[] bytes = md.digest(password.getBytes("utf-8"));
        return Arrays.copyOf(bytes, 16);
    }

    /**
     * 自定义HttpInputMessage,用于封装解密后的body体
     */
    static class DecryptedHttpInputMessage implements HttpInputMessage {
        private HttpHeaders headers;
        private InputStream body;

        public DecryptedHttpInputMessage(HttpHeaders headers, byte[] decryptedBody) {
            this.body = new ByteArrayInputStream(decryptedBody);
            this.headers = headers;
        }

        @Override
        public InputStream getBody() throws IOException {
            return this.body;
        }

        @Override
        public HttpHeaders getHeaders() {
            return this.headers;
        }
    }
} 

在使用时,我们只需要在被注解的方法上添加 @RequestBody,并在需要加密的RequestBody类型上添加 @Encrypt注解即可:

@PostMapping("/hello")
public String hello(@RequestBody @Encrypt HelloRequest request) {
    return request.getName() + " say hello to " + request.getTarget();
}

以上代码中,HelloRequest为需要加密的RequestBody类型,@Encrypt为自己定义的注解。

示例二:RequestBody参数验证

这里我们通过Spring提供的校验注解进行RequestBody参数校验:

@Data
public class UserRequest {
    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotNull(message = "年龄不能为空")
    @Min(value = 18, message = "年龄必须大于18岁")
    private Integer age;
}
@ControllerAdvice
public class RequestBodyValidationAdvice implements RequestBodyAdvice {
    @Autowired
    private Validator validator;

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 这里我们只做对象类型的判断
        return Object.class.equals(targetType);
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {

        Object body = StreamUtils.copyToString(inputMessage.getBody(), Charset.forName("UTF-8"));

        Set<ConstraintViolation<Object>> validations = validator.validate(JsonUtils.fromJson(body.toString(), Object.class));
        if(validations.size() > 0)
            throw new BindException(validations.iterator().next().getMessage());

        return inputMessage;
    }

    /*
     * 其余接口省略...
     */
}

在使用时,我们只需要在被注解的方法上添加 @RequestBody,并在需要加密的RequestBody类型上添加相关的校验注解即可:

@PostMapping("/user")
public User addUser(@RequestBody @Valid UserRequest request) {
    return userService.addUser(User.builder().username(request.getUsername()).age(request.getAge()).build());
}

以上代码中,UserRequest为需要验证的RequestBody类型,@Valid为Spring提供的校验注解,方便我们快速从错误中找到需要校验出错的字段。

至此,使用RequestBodyAdvice进行统一参数处理的操作完成啦!