springboot修改接口入参出参实现入参加密出参解密
一、背景
针对项目已经开发完的接口,都需要加上传输数据加密的功能,对接口入参进行AES解密,对接口出参进行加密。考虑到尽量改动少点,使用自定义注解结合springmvc里的RequestBodyAdvice和ResponseBodyAdvice两个类进行实现。
RequestBodyAdvice允许针对接口请求体被读取之前进行修改,ResponseBodyAdvice允许接口出参在被返回之前进行修改。
二、实现
1、新建两个自定义注解类,用来标记哪些接口需要进行加密解密。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Encrypt { }
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.PARAMETER}) @Documented public @interface Decrypt { }
注意:@Decrypt配置的作用域是方法和参数上,@Encrypt则是只在方法上。
2、新建自定义DecryptRequestAdvice类继承RequestBodyAdviceAdapter,进行入参解密
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.client.utils.JSONUtils;
import com.google.common.base.Throwables;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
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.RequestBodyAdviceAdapter;
import java.lang.reflect.Type;
/**
* @Author: 夏威夷8080
* @Date: 2000/7/8 20:28
*/
@ControllerAdvice
@Slf4j
public class DecryptRequestAdvice extends RequestBodyAdviceAdapter {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
try {
String requestBody = JSONUtils.serializeObject(body);
if (JSONObject.isValid(requestBody)) {
JSONObject jsonObject = JSONObject.parseObject(requestBody);
String encryptData = jsonObject.getString("encryptData");
if (StringUtils.isBlank(encryptData)) {
throw new IllegalArgumentException("缺少加密内容!");
}
log.info("接口解密入参数据");
String decryptData = new AesUtil().decryptByHex(encryptData);
// String decryptData = Base64.decodeStr(encryptData);
// log.info("接口解密后的入参:{}", decryptData);
body = JSONObject.parseObject(decryptData, targetType);
} else {
// log.error("获取到的入参不是合法的json格式!");
throw new IllegalArgumentException("获取到的入参不是合法的json格式!");
}
} catch (Exception e) {
log.error("接口入参解密出错:{}", Throwables.getStackTraceAsString(e));
}
return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
}
3、新建自定义EncryptResponseAdvice类继承ResponseBodyAdvice,进行出参加密
这里的R是自定义的接口返回封装类
import com.alibaba.nacos.client.utils.JSONUtils;
import com.google.common.base.Throwables;
import lombok.extern.slf4j.Slf4j;
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;
/**
* @Author: 夏威夷8080
* @Date: 2000/7/8 19:55
*/
@ControllerAdvice
@Slf4j
public class EncryptResponseAdvice implements ResponseBodyAdvice<R> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.hasMethodAnnotation(Encrypt.class);
}
@Override
public R beforeBodyWrite(R body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
try {
if (body.getData() != null) {
log.info("接口加密返回的数据");
// log.info("接口加密前返回的数据:{}", JSONUtils.serializeObject(body.getData()));
String encStr = new AesUtil().encryptByHex(JSONUtils.serializeObject(body.getData()));
// String encStr = Base64.encode(JSONUtil.toJsonStr(body.getData()));
// log.info("接口加密后返回的数据:{}", encStr);
body.setData(encStr);
}
} catch (Exception e) {
log.error("接口返回数据加密出错:{}", Throwables.getStackTraceAsString(e));
}
return body;
}
}
4、controller接口
@PostMapping("/test")
@ApiOperation(value = "测试接口加密解密")
@Encrypt
public R<UserInfoDTO> test(@Decrypt @RequestBody @Valid QueryVO vo) {
UserInfoDTO convert = Convert.convert(UserInfoDTO.class, vo);
return new R(convert);
}
5、AES加解密工具类
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.HexUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.util.Arrays;
/**
*/
@Component
public class AesUtil {
/**
* @author ngh
* AES128 算法
* <p>
* CBC 模式
* <p>
* PKCS7Padding 填充模式
* <p>
* CBC模式需要添加一个参数iv
* <p>
* 介于java 不支持PKCS7Padding,只支持PKCS5Padding 但是PKCS7Padding 和 PKCS5Padding 没有什么区别
* 要实现在java端用PKCS7Padding填充,需要用到bouncycastle组件来实现
*/
private Key key;
private Cipher cipher;
boolean isInited = false;
String aesKey = "0325mlm2022";
byte[] iv = "0103021405060878".getBytes();
byte[] keyBytes = aesKey.getBytes();
public void init(byte[] keyBytes) {
// 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
int base = 16;
if (keyBytes.length % base != 0) {
int groups = keyBytes.length / base + (keyBytes.length % base != 0 ? 1 : 0);
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyBytes, 0, temp, 0, keyBytes.length);
keyBytes = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
// 转化成JAVA的密钥格式
key = new SecretKeySpec(keyBytes, CipherType.AES_ALGORITHM);
try {
// 初始化cipher
cipher = Cipher.getInstance(CipherType.AES_CBC_PKC7PADDING, "BC");
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchPaddingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchProviderException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public String encrypt(String content) {
return Base64.encode(encrypt(content.getBytes(), keyBytes));
}
public String encrypt(String content, String keyBytes) {
return Base64.encode(encrypt(content.getBytes(), keyBytes.getBytes()));
}
public String encryptByHex(String content, String keyBytes) {
return HexUtil.encodeHexStr(encrypt(content.getBytes(), keyBytes.getBytes()));
}
public String encryptByHex(String content) {
return HexUtil.encodeHexStr(encrypt(content.getBytes(), keyBytes));
}
/**
* 加密方法
*
* @param content 要加密的字符串
* @param keyBytes 加密密钥
* @return
*/
public byte[] encrypt(byte[] content, byte[] keyBytes) {
byte[] encryptedText = null;
keyBytes = new String(keyBytes).getBytes();
init(keyBytes);
try {
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
encryptedText = cipher.doFinal(content);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return encryptedText;
}
public String decrypt(String encryptedData) {
return new String(decrypt(Base64.decode(encryptedData), keyBytes));
}
public String decrypt(String encryptedData, String keyData) {
return new String(decrypt(Base64.decode(encryptedData), keyData.getBytes()));
}
public String decryptByHex(String encryptedData, String keyData) {
return new String(decrypt(HexUtil.decodeHex(encryptedData), keyData.getBytes()));
}
public String decryptByHex(String encryptedData) {
return new String(decrypt(HexUtil.decodeHex(encryptedData), keyBytes));
}
/**
* 解密方法
*
* @param encryptedData 要解密的字符串
* @param keyBytes 解密密钥
* @return
*/
public byte[] decrypt(byte[] encryptedData, byte[] keyBytes) {
byte[] encryptedText = null;
init(keyBytes);
try {
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
encryptedText = cipher.doFinal(encryptedData);
} catch (Exception e) {
e.printStackTrace();
}
return encryptedText;
}
public static void main(String[] args) throws IOException {
AesUtil aes = new AesUtil();
//加密字符串
String content = "{\n" +
"\"start\":\"2022-07-20 08:00:35\",\n" +
"\"end\":\"2022-07-20 16:00:37\",\n" +
" \"page\": 1,\n" +
" \"pageSize\": 10\n" +
"}";
System.out.println("加密前的:" + content);
// 加密方法
String encStr = aes.encryptByHex(content);
System.out.println("加密后的内容:" + encStr);
// 解密方法
String decStr = aes.decryptByHex("7807418b6840a6");
System.out.println("解密后的内容:" + decStr);
}
}
6、常量类
/**
* @Version: 1.0
*/
public class CipherType {
//MD5
public final static String MD5 = "MD5";
//sha
public final static String SHA_1 = "SHA-1";
public final static String SHA_256 = "SHA-256";
//HMAC
public final static String HMAC_SHA_1 = "HmacSHA1";
public final static String HMAC_SHA_256 = "HmacSHA256";
//AES
public final static String AES_ALGORITHM = "AES";
public final static String AES_CBC_PKC5PADDING = "AES/CBC/PKCS5Padding";
public final static String AES_CBC_PKC7PADDING = "AES/CBC/PKCS7Padding";
public final static String AES_ECB_PKC7PADDING = "AES/ECB/PKCS7Padding";
public final static String AES_CBC_NODDING = "AES/CBC/NoPadding"; //NoPadding非填充,明文必须是16的整数倍
public final static String AES_ECB_PKC5PADDING = "AES/ECB/PKCS5Padding"; //ECB模式,IV不要填
public final static String AES_ECB_NODDING = "AES/ECB/NoPadding";
//RSA
public final static String RSA = "RSA";
//RSA加密算法
public final static String RSA_ECB_PSCS1PADDING = "RSA/ECB/PKCS1Padding";
//
// public final static String RSA_ECB_PSCS1PADDING = "RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING";
//签名算法
public final static String SHA256_RSA = "SHA256withRSA";
}
三、原理说明
上面那两个advice类型,都要使用@ControllerAdvice注解进行修饰,它其实是一个实现特殊功能的@Component,只是针对controller进行拦截,本质还是aop,我们平常使用的全局异常处理类@ExceptionHandler,GlobalExceptionHandler,也是配合该注解使用。
另外这边入参拦截修改,只针对@RequestBody修饰的body进行处理,同时返回一样,要被@ResponseBody修饰,如果你使用的是@RestController,那就不需要再加了。
浙公网安备 33010602011771号