使用@ResponseBodyAdvice统一对响应的数据进行处理

使用@ResponseBodyAdvice统一对响应的数据进行处理

开发遇到一个需求,就是对响应给APP的json数据进行加密处理。知道 @ResponseBodyAdvice 可以干这事儿,但是一直没用过。所以,这次专门学习了一下

ResponseBodyAdvice

public interface ResponseBodyAdvice<T> {
	/**
	 * @param returnType 响应的数据类型
	 * @param converterType 最终将会使用的消息转换器
	 * @return 返回bool,表示是否要在响应之前执行“beforeBodyWrite” 方法
	 */
	boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

	/**
	 * @param body 响应的数据,也就是响应体
	 * @param returnType 响应的数据类型
	 * @param selectedContentType 响应的ContentType
	 * @param selectedConverterType 最终将会使用的消息转换器
	 * @param request
	 * @param response
	 * @return 被修改后的响应体,可以为null,表示没有任何响应
	 */
	@Nullable
	T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
			Class<? extends HttpMessageConverter<?>> selectedConverterType,
			ServerHttpRequest request, ServerHttpResponse response);

}

蛮简单的东西,通过泛型,指定需要被“拦截”的响应体对象类型。

该接口的实现会在 Controller 方法返回数据,并且匹配到了 HttpMessageConverter 之后。HttpMessageConverter 进行序列化之前执行。可以通过覆写 beforeBodyWrite 来统一的对响应体进行修改。

注意,需要通过标识 @ControllerAdvice 注解激活。

Demo

EncodeResponseBodyAdvice

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
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;


import com.google.gson.Gson;

import io.springboot.common.Message;
import io.springboot.common.exception.ServiceException;
import io.springboot.common.utils.AESUtils;
/**
 * 
 * 对响应体进行加密
 * @author Administrator
 *
 */
@ControllerAdvice
public class EncodeResponseBodyAdvice implements ResponseBodyAdvice<Message<Object>> {
	
	static final Logger LOGGER = LoggerFactory.getLogger(EncodeResponseBodyAdvice.class);
	
	@Autowired
	Gson gson;
	
	/**
	 * 128位AES密钥
	 */
	public static final byte[] KEY = "9f5d54580044d478".getBytes();

	@Override
	public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
		/**
		 * 返回对象必须是  Message 并且使用了 GsonHttpMessageConverter 
		 */
		return GsonHttpMessageConverter.class.isAssignableFrom(converterType);
	}

	@Override
	public Message<Object> beforeBodyWrite(Message<Object> body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
		Object data = body.getData();
		if (data != null) {
			/**
			 * 重写data,进行加密
			 */
			body.setData(this.encode(data));
		}
		return body;
	}
	
	private String encode(Object data) {
		String jsonValue = gson.toJson(data);
		try {
			String cipher = Base64.getEncoder()
					.encodeToString(AESUtils.encrypt(jsonValue.getBytes(StandardCharsets.UTF_8), KEY));
			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("响应体AES加密:raw={},cipher={}", jsonValue, cipher);
			}
			return cipher;
		} catch (Exception e) {
			throw new ServiceException(Message.fail(Message.Code.INTERNAL_SERVER_ERROR, "对数据加密异常:" + e.getMessage()), e);
		}
	}
}

AESUtils

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

/**
 * 
 * AES
 * 
 *
 */
public class AESUtils {

	private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";

	// 获取 cipher
	private static Cipher getCipher(byte[] key, int model) throws Exception {
		SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
		Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
		cipher.init(model, secretKeySpec);
		return cipher;
	}

	// AES加密
	public static byte[] encrypt(byte[] data, byte[] key)
			throws Exception, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
		Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE);
		return cipher.doFinal(data);
	}

	// AES解密
	public static byte[] decrypt(byte[] data, byte[] key) throws Exception, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
		Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
		return cipher.doFinal(data);
	}
}

演示

Controller

@RequestMapping("/test")
@RestController
public class TestController {
	
	@GetMapping
	public Object test () {
		return Message.success(Collections.singletonMap("name", "SpringBoot中文社区"));
	}
}

加密后的内容

image

成功解密

RequestBodyAdvice

有响应,就一定有请求。@RequestBodyAdvice 方法是多了一些,但是万变不离其宗。稍微阅读,便能知其意。

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);


}

我使用RequestBodyAdvice时,当inputMessage.getBody().available()过大时,会对内容进行截断,无法读取到里面所有的内容。
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
try {
if (inputMessage.getBody().available() > 7000){ //处理上传签名时,传参过大KTECHttpInputMessage无法处理的问题
** logger.warn(“this input request param is so much.”);**
** return inputMessage;**
** }**
return new KTECHttpInputMessage(inputMessage);
} catch (Exception e) {
logger.error(“decrypt message error.method name: {}”, parameter.getMethod().getName());
return inputMessage;
}
}

啥意思?