在系统开发的过程中我们经常需要对外提供相应的API接口,为了保证系统数据的安全性,我们常常需要对传输的数据进行对称的加密。防止数据在传输的过程中被抓包,造成信息的泄露。通常的做法是我们在每个接口方法的前面先对请求的数据进行解密,解密完成后处理相应的业务逻辑,然后在对返回数据进行加密。这样做的坏处是代码太过于冗余,每写一个接口都要处理加密和解密方法。有没有什么办法可以把加密和解密的逻辑提取出来,在接口的方法中我们只关注处理业务逻辑。答案肯定是有的,springboot中的RequestBodyAdvice 和ResponseBodyAdvice就可以很好的解决这个问题。使用这两个的前提是方法中需要由@RequestBody,@ResponseBody注解,否则无效。
RequestBodyAdvice:主要是在HttpMessageConverter处理request body的前后做一些处理和body为空的时候做处理。RequestBodyAdvice有如下三个方法。只有 supports 方法返回true才会执行后面的几个方法。我们需要在请求达到方法之前对参数做一些处理,我们可以重新beforeBodyRead方法。
public interface RequestBodyAdvice {
boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType);
Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
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);
}
ResponseBodyAdvice:ResponseBodyAdvice 接口是在 Controller 执行 return 之后,在 response 返回给客户端之前,执行对 response 的一些处理。RequestBodyAdvice有如下两个方法。只有supports方法返回true才会执行beforeBodyWrite。我们需要在response 返回给客户端之前对返回的数据进行加密可以重写beforeBodyWrite方法。
public interface ResponseBodyAdvice<T> {
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
T beforeBodyWrite(T body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
}
我们先写报文解密的方法,新建一个EncryptRequestBodyAdvice类,继承RequestBodyAdvice,实现supports,handleEmptyBody,beforeBodyRead,afterBodyRead方法。并且在类上面添加@ControllerAdvice注解。supports返回true,handleEmptyBody和afterBodyRead不做任何处理,直接返回body。我们在beforeBodyRead方法中对报文进行解密:
@Slf4j
@ControllerAdvice
public class EncryptRequestBodyAdvice implements RequestBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, 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 {
try {
return new DecryptHttpInputMessage(inputMessage);
} catch (Exception e) {
log.error("数据解密失败", e);
}
return inputMessage;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}
@Slf4j
class DecryptHttpInputMessage implements HttpInputMessage {
private HttpHeaders headers;
private InputStream body;
public DecryptHttpInputMessage(HttpInputMessage inputMessage) throws Exception {
this.headers = inputMessage.getHeaders();
String content = IOUtils.toString(inputMessage.getBody(), "utf-8");
log.info("请求加密前报文:{}", content);
content = AesUtil.decrypt(content, "123456");
log.info("请求解密报文:{}", content);
this.body = IOUtils.toInputStream(content, "utf-8");
}
@Override
public InputStream getBody() throws IOException {
return body;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}
然后我们在写报文加密的方法,新建一个EncryptResponseBodyAdvice类,继承ResponseBodyAdvice<Object>。重写beforeBodyWrite,和supports方法,并且在类上面添加@ControllerAdvice注解。Supports方法返回true,我们在beforeBodyWrite 方法中对数据进行加密。
@Slf4j
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {
private Logger logger = LoggerFactory.getLogger(EncryptResponseBodyAdvice.class);
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public Object beforeBodyWrite(Object arg0, MethodParameter arg1, MediaType arg2,
Class<? extends HttpMessageConverter<?>> arg3, ServerHttpRequest arg4,
ServerHttpResponse arg5) {
String content = "";
try {
content = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(arg0);
log.info("返回报文加密前:{}", content);
content = AesUtil.encrypt(content, "123456");
log.info("返回报文加密后:{}", content);
} catch (Exception e1) {
logger.error("返回报文转换异常", e1);
}
return content;
}
@Override
public boolean supports(MethodParameter arg0, Class<? extends HttpMessageConverter<?>> arg1) {
return true;
}
}
这里我们使用的是AES加密解密,方法如下:
public class AesUtil {
/**
* 加密
*/
public static String encrypt(String text, String password) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
byte[] byteContent = text.getBytes("utf-8");
cipher.init(1, genKey(password));
byte[] result = cipher.doFinal(byteContent);
return parseByte2HexStr(result);
}
/**
* 解密
*/
public static String decrypt(String encryptText, String password) throws Exception {
byte[] decryptFrom = parseHexStr2Byte(encryptText);
Cipher cipher = Cipher.getInstance("AES");
cipher.init(2, genKey(password));
byte[] result = cipher.doFinal(decryptFrom);
return new String(result);
}
private static SecretKeySpec genKey(String password) throws NoSuchAlgorithmException {
byte[] enCodeFormat = { 0 };
KeyGenerator kgen = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(password.getBytes());
kgen.init(128, secureRandom);
SecretKey secretKey = kgen.generateKey();
enCodeFormat = secretKey.getEncoded();
return new SecretKeySpec(enCodeFormat, "AES");
}
private static String parseByte2HexStr(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
private static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1) {
return null;
}
byte[] result = new byte[hexStr.length() / 2];
for (int i = 0; i < hexStr.length() / 2; i++) {
int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
result[i] = ((byte) (high * 16 + low));
}
return result;
}
public static void main(String[] args) throws Exception {
String content = "{\n" + "\"id\": 1,\n" + "\"name\": \"张三\",\n" + "\"address\":\"上海市浦东新区\"\n" + "}";
System.out.println(AesUtil.encrypt(content, "123456"));
}
}
然后我们在写一个Controller方法,方法很简单就是把接收到的参数直接返回如下:
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/getuser")
public User getuser(@RequestBody User user) {
return user;
}
}
使用postman进行测试如下:
在body中输入AES加密后的密文进行请求:
后台打印日志如下:
20211122号补充:
上面我们可以看到返回前端的时候多了双引号,可以重写Jackson消息转换器的writeInternal方法,解决ResponseBodyAdvice返回字符串前后多两个问号问题。
@Configuration
public class MessageConverterConfig extends WebMvcConfigurationSupport {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//把重写的顺序放在第六位,因为默认有5个(小标从0开始)详细见WebMvcConfigurationSupport的addDefaultHttpMessageConverters方法
converters.add(5, mappingJackson2HttpMessageConverter());
}
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
return new MappingJackson2HttpMessageConverter() {
/**
* 重写Jackson消息转换器的writeInternal方法,解决ResponseBodyAdvice返回字符串前后多两个问号问题
* SpringMVC选定了具体的消息转换类型后,会调用具体类型的write方法,将Java对象转换后写入返回内容
*/
@Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
if (object instanceof String) {
//在ResponseBodyAdvice进行转换时返回值变成String了,不能用原来选定消息转换器进行转换,直接使用StringHttpMessageConverter转换
//StringHttpMessageConverter中就是用以下代码写的
Charset charset = this.getContentTypeCharset(outputMessage.getHeaders().getContentType());
StreamUtils.copy((String) object, charset, outputMessage.getBody());
} else {
//返回值不是String类型,还是使用之前选择的转换器进行消息转换
super.writeInternal(object, type, outputMessage);
}
}
private Charset getContentTypeCharset(MediaType contentType) {
return contentType != null && contentType.getCharset() != null ? contentType.getCharset()
: this.getDefaultCharset();
}
};
}
}