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

四、问题解决

参见《SpringMvc里的RequestBodyAdviceAdapter使用问题

posted @   夏威夷8080  阅读(3532)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
历史上的今天:
2018-07-10 linux命令useradd添加用户详解
点击右上角即可分享
微信分享提示