注解主要用来指定那些需要加解密的controller方法
实现比较简单
1
2
3
4
5
6
|
@Target ({ElementType.METHOD}) @Retention (RetentionPolicy.RUNTIME) public @interface SecretAnnotation { boolean encode() default false ; boolean decode() default false ; } |
使用时添加注解在controller的方法上
1
2
3
4
5
|
@PostMapping ( "/preview" ) @SecretAnnotation (decode = true ) public ResponseVO<ContractSignVO> previewContract( @RequestBody FillContractDTO fillContractDTO) { return contractSignService.previewContract(fillContractDTO); } |
请求数据由二进制流转为类对象数据,对于加密过的数据,需要在二进制流被处理之前进行解密,否则在转为类对象时会因为数据格式不匹配而报错。
因此使用RequestBodyAdvice的beforeBodyRead方法来处理。
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
|
@Slf4j @RestControllerAdvice public class MyRequestControllerAdvice implements RequestBodyAdvice { @Override public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { return methodParameter.hasParameterAnnotation(RequestBody. class ); } @Override public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { return o; } @Autowired private MySecretUtil mySecretUtil; @Override public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException { if (methodParameter.getMethod().isAnnotationPresent(SecretAnnotation. class )) { SecretAnnotation secretAnnotation = methodParameter.getMethod().getAnnotation(SecretAnnotation. class ); if (secretAnnotation.decode()) { return new HttpInputMessage() { @Override public InputStream getBody() throws IOException { List<String> appIdList = httpInputMessage.getHeaders().get( "appId" ); if (appIdList.isEmpty()){ throw new RuntimeException( "请求头缺少appID" ); } String appId = appIdList.get( 0 ); String bodyStr = IOUtils.toString(httpInputMessage.getBody(), "utf-8" ); bodyStr = mySecretUtil.decode(bodyStr,appId); return IOUtils.toInputStream(bodyStr, "utf-8" ); } @Override public HttpHeaders getHeaders() { return httpInputMessage.getHeaders(); } }; } } return httpInputMessage; } @Override public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { return o; } } |
mySecretUtil.decode(bodyStr,appId)的内容是,通过请求头中的AppID去数据库中查找对于的秘钥,之后进行解密,返回解密后的字符串。
再通过common.io包中提供的工具类IOUtils将字符串转为inputstream流,替换HttpInputMessage,返回一个body数据为解密后的二进制流的HttpInputMessage。
Stringboot RequestBodyAdvice接口如何实现请求响应加解密
在实际项目中,我们常常需要在请求前后进行一些操作,比如:参数解密/返回结果加密,打印请求参数和返回结果的日志等。这些与业务无关的东西,我们不希望写在controller方法中,造成代码重复可读性变差。这里,我们讲讲使用@ControllerAdvice和RequestBodyAdvice、ResponseBodyAdvice来对请求前后进行处理(本质上就是AOP),来实现日志记录每一个请求的参数和返回结果。
1.加解密工具类
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
|
package com.linkus.common.utils; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Security; import javax.annotation.PostConstruct; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Hex; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class Aes { /** * * @author ngh * AES128 算法 * * CBC 模式 * * PKCS7Padding 填充模式 * * CBC模式需要添加偏移量参数iv,必须16位 * 密钥 sessionKey,必须16位 * * 介于java 不支持PKCS7Padding,只支持PKCS5Padding 但是PKCS7Padding 和 PKCS5Padding 没有什么区别 * 要实现在java端用PKCS7Padding填充,需要用到bouncycastle组件来实现 */ private String sessionKey= "加解密密钥" ; // 偏移量 16位 private static String iv= "偏移量" ; // 算法名称 final String KEY_ALGORITHM = "AES" ; // 加解密算法/模式/填充方式 final String algorithmStr = "AES/CBC/PKCS7Padding" ; // 加解密 密钥 16位 byte [] ivByte; byte [] keybytes; private Key key; private Cipher cipher; boolean isInited = false ; public void init() { // 如果密钥不足16位,那么就补足. 这个if 中的内容很重要 keybytes = iv.getBytes(); ivByte = iv.getBytes(); Security.addProvider( new BouncyCastleProvider()); // 转化成JAVA的密钥格式 key = new SecretKeySpec(keybytes, KEY_ALGORITHM); try { // 初始化cipher cipher = Cipher.getInstance(algorithmStr, "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(); } } /** * 加密方法 * * @param content * 要加密的字符串 * 加密密钥 * @return */ public String encrypt(String content) { byte [] encryptedText = null ; byte [] contentByte = content.getBytes(); init(); try { cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(ivByte)); encryptedText = cipher.doFinal(contentByte); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return new String(Hex.encode(encryptedText)); } /** * 解密方法 * * @param encryptedData * 要解密的字符串 * 解密密钥 * @return */ public String decrypt(String encryptedData) { byte [] encryptedText = null ; byte [] encryptedDataByte = Hex.decode(encryptedData); init(); try { cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivByte)); encryptedText = cipher.doFinal(encryptedDataByte); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return new String(encryptedText); } public static void main(String[] args) { Aes aes = new Aes(); String a= "{\n" + "\"distance\":\"1000\",\n" + "\"longitude\":\"28.206471\",\n" + "\"latitude\":\"112.941301\"\n" + "}" ; //加密字符串 //String content = "孟飞快跑"; // System.out.println("加密前的:" + content); // System.out.println("加密密钥:" + new String(keybytes)); // 加密方法 String enc = aes.encrypt(a); System.out.println( "加密后的内容:" + enc); String dec= "" ; // 解密方法 try { dec = aes.decrypt(enc); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println( "解密后的内容:" + dec); } } |
2.请求解密
前端页面传过来的是密文,我们需要在Controller获取请求之前对密文解密然后传给Controller
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
|
package com.linkus.common.filter; import com.alibaba.fastjson.JSON; import com.linkus.common.constant.KPlatResponseCode; import com.linkus.common.exception.CustomException; import com.linkus.common.exception.JTransException; import com.linkus.common.service.util.MyHttpInputMessage; import com.linkus.common.utils.Aes; import com.linkus.common.utils.http.HttpHelper; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice; import javax.servlet.http.HttpServletRequest; import java.io.*; import java.lang.reflect.Type; /** * 请求参数 解密操作 * * @Author: Java碎碎念 * @Date: 2019/10/24 21:31 * */ @Component //可以配置指定需要解密的包,支持多个 @ControllerAdvice (basePackages = { "com.linkus.project" }) @Slf4j public class DecryptRequestBodyAdvice implements RequestBodyAdvice { Logger log = LoggerFactory.getLogger(getClass()); Aes aes= new Aes(); @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { //true开启功能,false关闭这个功能 return true ; } //在读取请求之前做处理 @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> selectedConverterType) throws IOException { //获取请求数据 String string = "" ; BufferedReader bufferedReader = null ; InputStream inputStream = inputMessage.getBody(); //这个request其实就是入参 可以从这里获取流 //入参放在HttpInputMessage里面 这个方法的返回值也是HttpInputMessage try { string=getRequestBodyStr(inputStream,bufferedReader); } finally { if (bufferedReader != null ) { try { bufferedReader.close(); } catch (IOException ex) { throw ex; } } } /*****************进行解密start*******************/ String decode = null ; if (HttpHelper.isEncrypted(inputMessage.getHeaders())){ try { // //解密操作 //Map<String,String> dataMap = (Map)body; //log.info("接收到原始请求数据={}", string); // inputData 为待加解密的数据源 //解密 decode= aes.decrypt(string); //log.info("解密后数据={}",decode); } catch (Exception e ) { log.error( "加解密错误:" ,e); throw new CustomException(KPlatResponseCode.MSG_DECRYPT_TIMEOUT,KPlatResponseCode.CD_DECRYPT_TIMEOUT); } //把数据放到我们封装的对象中 } else { decode = string; } // log.info("接收到请求数据={}", decode); // log.info("接口请求地址{}",((HttpServletRequest)inputMessage).getRequestURI()); return new MyHttpInputMessage(inputMessage.getHeaders(), new ByteArrayInputStream(decode.getBytes( "UTF-8" ))); } @Override public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return body; } @Override public Object handleEmptyBody( @Nullable Object var1, HttpInputMessage var2, MethodParameter var3, Type var4, Class<? extends HttpMessageConverter<?>> var5) { return var1; } //自己写的方法,不是接口的方法,处理密文 public String getRequestBodyStr( InputStream inputStream,BufferedReader bufferedReader) throws IOException { StringBuilder stringBuilder = new StringBuilder(); if (inputStream != null ) { bufferedReader = new BufferedReader( new InputStreamReader(inputStream)); char [] charBuffer = new char [ 128 ]; int bytesRead = - 1 ; while ((bytesRead = bufferedReader.read(charBuffer)) > 0 ) { stringBuilder.append(charBuffer, 0 , bytesRead); } } else { stringBuilder.append( "" ); } String string = stringBuilder.toString(); return string; } } |
3.响应加密
将返给前端的响应加密,保证数据的安全性
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
|
package com.linkus.common.filter; import com.alibaba.fastjson.JSON; import com.linkus.common.utils.Aes; import com.linkus.common.utils.DesUtil; import com.linkus.common.utils.http.HttpHelper; import io.swagger.models.auth.In; import lombok.experimental.Helper; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.stereotype.Component; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 请求参数 加密操作 * * @Author: Java碎碎念 * @Date: 2019/10/24 21:31 * */ @Component @ControllerAdvice (basePackages = { "com.linkus.project" }) @Slf4j public class EncryResponseBodyAdvice implements ResponseBodyAdvice<Object> { Logger log = LoggerFactory.getLogger(getClass()); Aes aes= new Aes(); @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true ; } @Override public Object beforeBodyWrite(Object obj, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { String returnStr = "" ; Object retObj = null ; log.info( "接口请求地址{}" ,serverHttpRequest.getURI()); //日志过滤 //retObj=infofilter.getInfoFilter(returnType,obj); if (HttpHelper.isEncrypted(serverHttpRequest.getHeaders())) { try { //添加encry header,告诉前端数据已加密 //serverHttpResponse.getHeaders().add("infoe", "e=a"); //获取请求数据 String srcData = JSON.toJSONString(obj); //加密 returnStr = aes.encrypt(srcData).replace( "\r\n" , "" ); //log.info("原始数据={},加密后数据={}", obj, returnStr); return returnStr; } catch (Exception e) { log.error( "异常!" , e); } } log.info( "原始数据={}" ,JSON.toJSONString(obj)); return obj; } } |
以上为个人经验,希望能给大家一个参考,也希望大家多多支持服务器之家。
原文链接:https://blog.csdn.net/more_try/article/details/88682588