一文带你掌握SpringMVC扩展点RequestBodyAdvice和ResponseBodyAdvice如何使用及实现原理

1.概述

Spring MVC是当今web项目系统开发后端技术框架的不二之选,也是Spring全家桶中非常重要的一个成员,它的设计思想和实现套路都是很值得我们学习的,所以今天我们就再来看看Spring MVC框架中预留的两个钩子也就是扩展点:RequestBodyAdviceResponseBodyAdvice。之前在总结详解@ControllerAdvice的使用及其实现原理一文中就有提到这两个扩展类,它们需要配合@ControllerAdvice一起使用

1.1 RequestBodyAdvice

RequestBodyAdviceSpring MVC 框架中的一个接口,允许在 HTTP 请求的请求体(request body)被反序列化为 Java 对象之前进行拦截和修改。它为开发者提供了一个钩子,可以在请求处理过程中插入自定义的逻辑,例如对请求体进行预处理、验证或日志记录

1.1.1 源码定义如下:
public interface RequestBodyAdvice {
   
   

	boolean supports(MethodParameter methodParameter, Type targetType,
			Class<? extends HttpMessageConverter<?>> converterType);

	HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;

	Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

	@Nullable
	Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

}

  • supports():该方法用于确定是否应该应用这个 RequestBodyAdvice 实例。返回 true 表示支持当前的转换。
  • beforeBodyRead():在读取请求体之前调用,可以用于包装或修改传入的 HttpInputMessage
  • afterBodyRead():在请求体被读取并转换为 Java 对象之后调用,可以修改或替换读取到的对象。
  • handleEmptyBody():在请求体为空时调用,可以提供一个默认对象或处理空请求体的情况。
1.1.2 实际应用

在实际应用中,RequestBodyAdvice 可用于以下场景:

  • 日志记录:记录请求体的内容,便于调试和审计。
  • 预处理:在反序列化之前对请求体进行预处理,如解密或解码。
  • 验证:在反序列化之前对请求体进行验证,如检查 JSON 结构是否正确。
  • 默认值:处理空请求体并提供默认值。

1.2 ResponseBodyAdvice

ResponseBodyAdviceSpring MVC 框架中的一个接口,允许在响应体写入之前进行处理。它提供了一种方式,可以在 Spring MVC 将响应对象转换为 HTTP 响应体之前,对响应数据进行修改或处理。

1.2.1 源码定义如下:
public interface ResponseBodyAdvice<T> {
   
   

	boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

	@Nullable
	T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
			Class<? extends HttpMessageConverter<?>> selectedConverterType,
			ServerHttpRequest request, ServerHttpResponse response);

}
  • supports():这个方法用于判断是否要对给定的返回类型和转换器类型应用当前的 ResponseBodyAdvice 实现。返回 true 表示要应用,返回 false 表示不应用。

  • beforeBodyWrite():这个方法在响应体写入之前调用,允许对响应数据进行修改或处理。它返回处理后的响应数据。

1.2.2 实际应用

ResponseBodyAdvice 可以用于多种应用场景,例如:

  • 数据加密:在将响应数据返回给客户端之前对其进行加密。
  • 数据格式转换:根据客户端的请求参数动态调整响应数据的格式,例如 XML 转 JSON。
  • 统一响应格式:对所有 API 接口的响应数据进行统一包装,确保一致的响应格式。
  • 响应头设置:在返回响应体之前设置或修改响应头,例如添加自定义头部信息。

2.项目实战案例

2.1 使用ResquestBodyAdvice对接口入参进行加解密和验签

之前我们在总结Spring Boot如何优雅提高接口数据安全性一文中强调了接口数据安全的重要性,特别是对外提供的open api接口,肯定是不能让接口数据裸奔的。强烈建议点击链接跳转之前总结的文章仔细看看,可以了解需求功能背景和之前的AOP切面实现,再来和今天我们使用RequestBodyAdvice的实现对比一下两者的优劣势,这样就掌握了两个知识点。

Spring Boot项目中提高接口安全的核心所在:加密和加签,加固接口参数、验证复杂度。

**加密:**对参数进行加密传输,拒绝接口参数直接暴露,这样就可以有效做到防止别人轻易准确地获取到接口参数定义和传参格式要求了。

**加签:**对接口参数进行加签,可以有效防止接口参数被篡改和接口参数被重放恶刷。

闲话少续,这里假如你没有点击链接查看之前的需求功能背景,所以我简要叙述下之前需求功能实现:就是使用非对称加密算法RSA和对称加密算法AES对接口出入参数进行加解密。为啥使用这两种加密算法呢?

AES 是对称加密算法,优点:加密速度快;缺点:如果秘钥丢失,就容易解密密文,安全性相对比较差

RSA 是非对称加密算法 , 优点:安全 ;缺点:加密速度慢

加解密

具体步骤如下:

  1. 客户端(调用接口方)随机生成AES加解密的密钥aes key,这里的AES密钥每次调接口都需要随机生成,可以有效提高安全性。
  2. 使用aes key对接口参数requestBody进行加密,data=base64(AES(json参数))
  3. 通过RSA加密算法加密aes key,有效保证aes算法的密钥的可靠安全性 key=base64(RSA(aes key))
  4. 经过上面的步骤,得到了加密后的业务参数及密钥,这时候就可以发送请求调用接口了
  5. 服务端接收到请求之后,先通过RSA算法对key进行解密获取到ase key, 再通过aes key解密data得到真正json参数,最后映射到接口方法的参数对象上,供controller的业务方法逻辑使用。
  6. 业务方法执行完成后,对响应参数进行加密,加密流程和上面的1、2、3一样
  7. 客户端收到响应参数之后,和步骤5一样解密响应参数,就拿到了真正的数据结果了。

加签

具体流程如下

  1. 对请求参数对象paramsortMap保证参数拼接的有序性,如果接口没有参数也没有关系,这里转成一个空的sortMap
  2. 按照约定拼接生成字符串content = sortMap + nonce + timestamp
  3. 使⽤SHA1WithRSA算法及私钥对concent进⾏签名sign
  4. 服务端判断timestamp是否超过签名有效期和nonce是否重复使用
  5. 服务端和步骤2一样规则生成字符串content
  6. 使⽤SHA1WithRSA算法及公钥对concentsign进行验签

强烈建议跳转Spring Boot如何优雅提高接口数据安全性去看看可以了解的更清楚些,有流程图那些,更详细。

代码实现

注解ApiSecurity

/**
 * @author fjzheng
 * @version 1.0
 * @date 2023/5/2 11:50
 *
 * 该注解用于标识 需要经过加密或者加签来加固接口安全性的接口
 */

@Retention(RetentionPolicy.RUNTIME)
@Target({
   
   ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface ApiSecurity {
   
   

    @Alias("isSign")
    boolean value() default true;

    /**
     * 是否加签验证,默认开启
     * @return
     */
    @Alias("value")
    boolean isSign() default true;

    /**
     * 接口请求参数是否需要解密
     * @return
     */
    boolean decryptRequest() default false;

    /**
     * 接口响应参数是否需要加密
     * @return
     */
    boolean encryptResponse() default false;
}

RequestBodyAdvice实现

@RestControllerAdvice
public class RequestBodyHandlerAdvice implements RequestBodyAdvice {
   
   
    @Resource
    private ApiSecurityProperties apiSecurityProperties;
    @Resource
    private StringRedisTemplate stringRedisTemplate;


    private static final String SIGN_KEY = "X-Sign";
    private static final String NONCE_KEY = "X-Nonce";
    private static final String TIMESTAMP_KEY = "X-Timestamp";


    /**
     *
     * @param methodParameter 包含控制器方法的参数信息
     * @param targetType  目标类型,即请求体将要转换成的 Java 类型
     * @param converterType 将要使用的消息转换器的类型
     * @return
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
   
   
        return methodParameter.hasMethodAnnotation(ApiSecurity.class)
                || AnnotatedElementUtils.hasAnnotation(methodParameter.getDeclaringClass(), ApiSecurity.class);
    }

    /**
     * 接口入参解密
     * @param inputMessage 包含 HTTP 请求的头和体
     * @param parameter  包含控制器方法的参数信息
     * @param targetType  目标类型,即请求体将要转换成的 Java 类型
     * @param converterType  将要使用的消息转换器的类型
     * @return   返回新的流
     * @throws IOException
     */
    @Override
    public 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值