Spring之RequestBodyAdvice和ResponseBodyAdvice使用场景和原理

背景:

在实际项目中,我们常常需要在Controller请求前后进行一些操作,比如:参数解密/返回结果加密,打印请求参数和返回结果的日志等。

方式一:统一包装返回值

  1. 通常返回结果包含code、message、data,结构如下
  2. 缺点:每个Controller都要修改
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseResult<T> {
    private int code;
    private String message;
    private T data;
    public ResponseResult(T data) {
        this.data = data;
        this.code = 0;
        this.message = "success";
    }
}

方式二:使用RequestBodyAdvice和ResponseBodyAdvice对请求前后进行处理

基于ControllerAdvice和HttpMessageConverter实现

注解1:标记不加密处理ExcludeHttpBodyDecrypt

import java.lang.annotation.*;

/**
 * @author yin
 * @create 2022-6-1 14:52
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcludeHttpBodyDecrypt {

}

注解1:标记加密处理ExcludeHttpBodyDecrypt

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * RequestBodyHandler
 *
 * @Author yin
 * @create 2022-6-1 14:52
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HttpBodyDecrypt {
}

测试Controller接口

@RestController
@RequestMapping("/UniversityManagement")
public class BsBuUniversityInfoController {
    @HttpBodyDecrypt
    @PostMapping(value = "/BsBuUniversityInfo/testNoLogin", name = "测试接口")
    public Object test(@RequestBody BsBuUniversityInfoModel model) {
        BsBuUniversityInfo buUniversityInfo = new BsBuUniversityInfo();
        buUniversityInfo.setUniversityCode("00000");
        return buUniversityInfo;
    }
}

通过RequestBodyAdvice,拦截Controller对request解密,签名验证处理

/**
 * @author yin
 * @create 2022-6-1 10:09
 */

import cn.gewut.business.common.Annotation.ExcludeHttpBodyDecrypt;
import cn.gewut.business.common.Annotation.HttpBodyDecrypt;
import cn.gewut.business.common.Util.AESUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;

@ControllerAdvice
public class RequestAdvisor implements RequestBodyAdvice {

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        System.out.println("supports");
        //排除解密注解--包含@ExcludeHttpBodyDecrypt 返回false不执行
        boolean methodHasExcludeHttpBodyDecrypt = methodParameter.hasMethodAnnotation(ExcludeHttpBodyDecrypt.class);
        if (methodHasExcludeHttpBodyDecrypt) {
            return false;
        }
        //解密注解--包含@HttpBodyDecrypt 返回true 需要执行
        boolean methodHasHttpBodyDecrypt = methodParameter.hasMethodAnnotation(HttpBodyDecrypt.class);
        if (methodHasHttpBodyDecrypt) {
            return true;
        }
        //排除解密注解--声明类是否包含@ExcludeHttpBodyDecrypt 返回false不执行
        boolean classHasExcludeHttpBodyDecrypt = methodParameter.getDeclaringClass().getAnnotation(ExcludeHttpBodyDecrypt.class) != null;
        if (classHasExcludeHttpBodyDecrypt) {
            return false;
        }
        boolean classHasHttpBodyDecrypt = methodParameter.getDeclaringClass().getAnnotation(HttpBodyDecrypt.class) != null;
        if (classHasHttpBodyDecrypt) {
            return true;
        }
        return false;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        System.out.println("beforeBodyRead");

        if (inputMessage.getBody().available() <= 0) {
            return inputMessage;
        }
        byte[] requestDataByte = new byte[inputMessage.getBody().available()];
        inputMessage.getBody().read(requestDataByte);
        byte[] requestDataByteNew = null;
        try {
            // 解密
            requestDataByteNew  = AESUtils.decryptHandler(requestDataByte);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 使用解密后的数据,构造新的读取流
        InputStream rawInputStream = new ByteArrayInputStream(requestDataByteNew);
        return new HttpInputMessage() {
            @Override
            public HttpHeaders getHeaders() {
                return inputMessage.getHeaders();
            }

            @Override
            public InputStream getBody() throws IOException {
                return rawInputStream;
            }
        };
    }

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

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        System.out.println("handleEmptyBody");
        return null;
    }

}

通过ResponseBodyAdvice,拦截Controller对Response封装,统一返回对象

/**
 * @author yin
 * @create 2022-6-1 10:09
 */

import cn.gewut.business.common.Util.ResponseResult;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class ResponseAdvisor implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {
        System.out.println(methodParameter.toString());

        return o;
//        if (o instanceof ResponseResult) {
//            return o;
//        }
//        return new ResponseResult<>(o);
    }
}

用到的 AES加密,签名工具类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * @author yin
 * @create 2022-6-1 10:09
 */

public class AESUtils {

    public static final String AESKey = "key1234567890key";

    public static final String signKey = "123456";

    // 加密
    public static String Encrypt(String sSrc, String sKey) throws Exception {
        if (sKey == null) {
            System.out.print("Key为空null");
            return null;
        }
        // 判断Key是否为16位
        if (sKey.length() != 16) {
            System.out.print("Key长度不是16位");
            return null;
        }
        byte[] raw = sKey.getBytes("utf-8");
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");//"算法/模式/补码方式"
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
        byte[] encrypted = cipher.doFinal(sSrc.getBytes("utf-8"));
        return Base64.getEncoder().encodeToString(encrypted);//此处使用BASE64做转码功能,同时能起到2次加密的作用。
    }

    // 解密
    public static String Decrypt(String sSrc, String sKey) throws Exception {
        try {
            // 判断Key是否正确
            if (sKey == null) {
                System.out.print("Key为空null");
                return null;
            }
            // 判断Key是否为16位
            if (sKey.length() != 16) {
                System.out.print("Key长度不是16位");
                return null;
            }
            byte[] raw = sKey.getBytes("utf-8");
            SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec);
            byte[] encrypted1 = Base64.getDecoder().decode(sSrc);//先用base64解密
            try {
                byte[] original = cipher.doFinal(encrypted1);
                String originalString = new String(original, "utf-8");
                return originalString;
            } catch (Exception e) {
                System.out.println(e.toString());
                return null;
            }
        } catch (Exception ex) {
            System.out.println(ex.toString());
            return null;
        }
    }

    /**
     * AES解密 加MD5签名验证。
     *
     * @param requestBytes
     * @return
     */
    public static byte[] decryptHandler(byte[] requestBytes) throws Exception {
        if (requestBytes.length <= 0) {
            return new byte[0];
        }
        String requestData = new String(requestBytes, StandardCharsets.UTF_8);
        JSONObject jsonobj = JSON.parseObject(requestData);

        System.out.println("收到的加密数据:" + jsonobj);
        String encrypt = jsonobj.get("encrypt") == null ? "" : jsonobj.get("encrypt").toString();
        String decrypt = "";
        if (encrypt.length() > 0) {
            //解密参数
            decrypt = AESUtils.Decrypt(encrypt, AESKey);
            if (decrypt == null) {
                //解密失败,返回异常
                throw new IllegalArgumentException("解密失败");
            }
        }
        System.out.println("解密后的数据:" + decrypt);
        //获取并验证签名。
        String sign = jsonobj.get("sign") == null ? "" : jsonobj.get("sign").toString();
        String md5Data = Md5Utils.md5Digest(decrypt + signKey);
        if (!sign.equals(md5Data)) {
            //验签失败
            throw new IllegalArgumentException("验证签名失败");
        }
        //验证通过,返回解密后的参数
        return decrypt.getBytes(StandardCharsets.UTF_8);
    }


    /**
     * AES加密 加MD5签名验证。
     *
     * @param requestBytes
     * @return
     */
    public byte[] encryptHandler(byte[] requestBytes) throws Exception {
        if (requestBytes.length <= 0) {
            return new byte[0];
        }
        String requestData = new String(requestBytes, StandardCharsets.UTF_8);
        String encrypt = "";
        if (requestData.length() > 0) {
            //加密密文
            encrypt = AESUtils.Encrypt(requestData, AESKey);
            if (encrypt == null) {
                //加密失败,返回异常
                throw new IllegalArgumentException("加密失败");
            }
        }

        JSONObject jsonObj = new JSONObject();
        jsonObj.put("encrypt", encrypt);//加密数据
        jsonObj.put("sign", Md5Utils.md5Digest(requestData + signKey));//签名

        System.out.println("加密签名后数据:" + jsonObj);
        //验证通过,返回解密后的参数
        return jsonObj.toJSONString().getBytes(StandardCharsets.UTF_8);
    }

	//测试AES加密
    public static void main(String[] args) throws Exception {
        /*
         * 此处使用AES-128-ECB加密模式,key需要为16位。
         */
        String cKey = "key1234567890key";
        // 需要加密的字串
        String cSrc = "www.yin.com";
        System.out.println(cSrc);
        // 加密
        String enString = Encrypt(cSrc, cKey);
        System.out.println("加密后的字串是:" + enString);

        // 解密
        String DeString = Decrypt(enString, cKey);
        System.out.println("解密后的字串是:" + DeString);
        System.out.println("*******************************************");

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name", "小米");
        jsonObject.put("universityCode", "00000");
        System.out.println("发送的接口数据:" + jsonObject);


        AESUtils aesUtils = new AESUtils();
        //加密
        byte[] bytes = aesUtils.encryptHandler(jsonObject.toJSONString().getBytes());

        //解密
        byte[] bytes1 = aesUtils.decryptHandler(bytes);
    }
}

原理

RequestResponseBodyAdviceChain

1. 启动执行RequestResponseBodyAdviceChain()初始化

把RequestBodyAdvice,ResponseBodyAdvice实现类都加到RequestResponseBodyAdviceChain;

2. 每次请求执行beforeBodyRead(),

源码可以看出循环所有实现类,然后先执行advice.supports(parameter, targetType, converterType) 为true后,执行request = advice.beforeBodyRead(request, parameter, targetType, converterType);

3. 再执行afterBodyRead()

源码可以看出循环所有实现类,然后先执行advice.supports(parameter, targetType, converterType) 为true后,执行body = advice.afterBodyRead(body, inputMessage, parameter, targetType, converterType);

class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice<Object> {

	private final List<Object> requestBodyAdvice = new ArrayList<>(4);

	private final List<Object> responseBodyAdvice = new ArrayList<>(4);


	/**
	 * Create an instance from a list of objects that are either of type
	 * {@code ControllerAdviceBean} or {@code RequestBodyAdvice}.
	 */
	public RequestResponseBodyAdviceChain(@Nullable List<Object> requestResponseBodyAdvice) {
		this.requestBodyAdvice.addAll(getAdviceByType(requestResponseBodyAdvice, RequestBodyAdvice.class));
		this.responseBodyAdvice.addAll(getAdviceByType(requestResponseBodyAdvice, ResponseBodyAdvice.class));
	}

	@SuppressWarnings("unchecked")
	static <T> List<T> getAdviceByType(@Nullable List<Object> requestResponseBodyAdvice, Class<T> adviceType) {
		if (requestResponseBodyAdvice != null) {
			List<T> result = new ArrayList<>();
			for (Object advice : requestResponseBodyAdvice) {
				Class<?> beanType = (advice instanceof ControllerAdviceBean ?
						((ControllerAdviceBean) advice).getBeanType() : advice.getClass());
				if (beanType != null && adviceType.isAssignableFrom(beanType)) {
					result.add((T) advice);
				}
			}
			return result;
		}
		return Collections.emptyList();
	}


	@Override
	public boolean supports(MethodParameter param, Type type, Class<? extends HttpMessageConverter<?>> converterType) {
		throw new UnsupportedOperationException("Not implemented");
	}

	@Override
	public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
		throw new UnsupportedOperationException("Not implemented");
	}

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

		for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
			if (advice.supports(parameter, targetType, converterType)) {
				request = advice.beforeBodyRead(request, parameter, targetType, converterType);
			}
		}
		return request;
	}

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

		for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
			if (advice.supports(parameter, targetType, converterType)) {
				body = advice.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
			}
		}
		return body;
	}

	@Override
	@Nullable
	public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType,
			Class<? extends HttpMessageConverter<?>> converterType,
			ServerHttpRequest request, ServerHttpResponse response) {

		return processBody(body, returnType, contentType, converterType, request, response);
	}

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

		for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
			if (advice.supports(parameter, targetType, converterType)) {
				body = advice.handleEmptyBody(body, inputMessage, parameter, targetType, converterType);
			}
		}
		return body;
	}


	@SuppressWarnings("unchecked")
	@Nullable
	private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType,
			Class<? extends HttpMessageConverter<?>> converterType,
			ServerHttpRequest request, ServerHttpResponse response) {

		for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
			if (advice.supports(returnType, converterType)) {
				body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
						contentType, converterType, request, response);
			}
		}
		return body;
	}

	@SuppressWarnings("unchecked")
	private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {
		List<Object> availableAdvice = getAdvice(adviceType);
		if (CollectionUtils.isEmpty(availableAdvice)) {
			return Collections.emptyList();
		}
		List<A> result = new ArrayList<>(availableAdvice.size());
		for (Object advice : availableAdvice) {
			if (advice instanceof ControllerAdviceBean) {
				ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;
				if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) {
					continue;
				}
				advice = adviceBean.resolveBean();
			}
			if (adviceType.isAssignableFrom(advice.getClass())) {
				result.add((A) advice);
			}
		}
		return result;
	}

	private List<Object> getAdvice(Class<?> adviceType) {
		if (RequestBodyAdvice.class == adviceType) {
			return this.requestBodyAdvice;
		}
		else if (ResponseBodyAdvice.class == adviceType) {
			return this.responseBodyAdvice;
		}
		else {
			throw new IllegalArgumentException("Unexpected adviceType: " + adviceType);
		}
	}

}
  • 4
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值