注解主要用來指定那些需要加解密的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