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,进行入参解密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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是自定义的接口返回封装类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | 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加解密工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | 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、常量类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /** * @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,那就不需要再加了。
四、问题解决
标签:
springboot
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
2018-07-10 linux命令useradd添加用户详解