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,那就不需要再加了。