前vue后springboot端统一修改请求与响应中的标头及body的方法(双向加解密数据传输)
直接上代码
一、结果
二、过程
vue 端 request.js 核心代码,加解密工具类,请见前面的贴子
1 // request拦截器 2 service.interceptors.request.use(config => { 3 // 是否需要设置 token 4 const isToken = (config.headers || {}).isToken === false 5 // 是否需要防止数据重复提交 6 const isRepeatSubmit = (config.headers || {}).repeatSubmit === false 7 if (getToken() && !isToken) { 8 config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 9 } 10 // get请求映射params参数 11 if (config.method === 'get' && config.params) { 12 let url = config.url + '?' + tansParams(config.params) 13 url = url.slice(0, -1) 14 config.params = {} 15 config.url = url 16 } 17 18 // 1.动态生成密钥 des3 key 19 const DES3_KEY_SIZE = 24 20 let des3Key = genKey(DES3_KEY_SIZE, true) 21 // console.log("des3Key:",des3Key); 22 23 // 2.key 经过rsa ,服务器端公钥加密,再base64, 得到密文key:SecurityKey 24 let SecurityKey = RsaEncrypts(des3Key, true, true, false) 25 26 // 3.设置header:SecurityKey 27 config.headers['SecurityKey'] = SecurityKey 28 // console.log("SecurityKey:",SecurityKey); 29 30 // console.log("config.data:",config.data); 31 let plainData = JSON.stringify(config.data) 32 // console.log("plainData:", plainData); 33 let cipherData = '' 34 // 4.将传输body数据des3加密,生成密文body,再base64,再urlcode 35 if (config.data != 'undefined') { 36 cipherData = encrypt3DES('TripleDES', plainData, des3Key, CryptoJS.mode.CBC, CryptoJS.pad.Pkcs7, true, true, false) 37 // console.log("cipherData:", cipherData); 38 } 39 40 // 5. 将密文body,经过base64, SHA256 生成摘要,客户端私钥生成签名,再base64 41 let Authentication = RsaSignAuthentication(cipherData, 'PKCS#7', 'SHA256withRSA', true, true, false) 42 43 // 6.设置header:Authentication 44 config.headers['Authentication'] = Authentication 45 // console.log("Authentication:",Authentication); 46 // 7.避免特殊字符丢失 urlcode 47 cipherData = encodeURIComponent(cipherData) 48 config.data = cipherData 49 50 if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { 51 let requestObj = { 52 url: config.url, 53 data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, 54 time: new Date().getTime() 55 } 56 // console.log("发送数据:",requestObj.data); 57 58 const sessionObj = cache.session.getJSON('sessionObj') 59 if (sessionObj === undefined || sessionObj === null || sessionObj === '') { 60 cache.session.setJSON('sessionObj', requestObj) 61 } else { 62 const s_url = sessionObj.url // 请求地址 63 const s_data = sessionObj.data // 请求数据 64 const s_time = sessionObj.time // 请求时间 65 const interval = 1000 // 间隔时间(ms),小于此时间视为重复提交 66 if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { 67 const message = '数据正在处理,请勿重复提交' 68 console.warn(`[${s_url}]: ` + message) 69 return Promise.reject(new Error(message)) 70 } else { 71 cache.session.setJSON('sessionObj', requestObj) 72 } 73 } 74 } 75 return config 76 }, error => { 77 console.log(error) 78 Promise.reject(error) 79 }) 80 81 // 响应拦截器 82 service.interceptors.response.use(response => { 83 // 未设置状态码则默认成功状态 84 const code = response.data.code || 200 85 // 获取错误信息 86 const message = errorCode[code] || response.data.message || errorCode['default'] 87 88 // 注意服务器端有大写字符,但前端接收返回全部小写 89 let authentication = response.headers['authentication'] 90 let securityKey = response.headers.securitykey 91 // 不要用console.log("响应结果:"+response.headers); 返回[object object] 92 // console.log("响应body:",response.data); 93 94 if (authentication) { 95 let cipherData = decodeURIComponent(response.data) 96 // console.log('接收到服务器端返回body密文:', cipherData) 97 let verifyAuthentication = RsaVerifyAuthentication(cipherData, authentication, 'PKCS#7', 'MD5withRSA', true, 98 true, false) 99 //console.log('通过服务器端的公钥,验签结果:' + verifyAuthentication.toString()) 100 if (verifyAuthentication) { 101 // let key = '1234567890123456' 102 // let aesKey = Base64.encode(key); 103 104 // aesKey ="8A5EzyHtJjklPj4a9NWwXw=="; 105 // console.log('测试,得到aeskey:' + aesKey) 106 107 let aesKey = RsaDecrypts(securityKey, true, true, false) 108 // console.log('通过客户端的私钥解密得到明文key:', aesKey) 109 110 // let plain = '依芸 abcdef 123' 111 // let cipherData = encryptAES('AES', plain, aesKey, CryptoJS.mode.CBC, CryptoJS.pad.Pkcs7, true, true, false) 112 // console.log('测试,加密得到密文数据:', cipherData) 113 114 // let plainData = decryptAES('AES', cipherData, aesKey, CryptoJS.mode.CBC, CryptoJS.pad.Pkcs7, true, true, false) 115 // console.log('测试,解密得到明文数据:', plainData) 116 117 let plainData = decryptAES('AES', cipherData, aesKey, CryptoJS.mode.CBC, CryptoJS.pad.Pkcs7, true, true, false) 118 // console.log('通过解密密钥后再aes解密body密文得到明文body:', plainData) 119 120 response.data = JSON.parse(plainData) 121 // 转换成json对象 122 } 123 } 124 125 // 二进制数据则直接返回 126 if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') { 127 return response.data 128 } 129 130 if (code === 401) { 131 if (!isReloginShow) { 132 isReloginShow = true 133 MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { 134 confirmButtonText: '重新登录', 135 cancelButtonText: '取消', 136 type: 'warning' 137 } 138 ).then(() => { 139 isReloginShow = false 140 store.dispatch('LogOut').then(() => { 141 // 如果是登录页面不需要重新加载 142 if (window.location.hash.indexOf('#/login') != 0) { 143 location.href = '/index' 144 } 145 }) 146 }).catch(() => { 147 isReloginShow = false 148 }) 149 } 150 return Promise.reject('无效的会话,或者会话已过期,请重新登录。') 151 } else if (code === 500) { 152 Message({ 153 message: message, 154 type: 'error' 155 }) 156 return Promise.reject(new Error(message)) 157 } else if (code !== 200) { 158 Notification.error({ 159 title: message 160 }) 161 return Promise.reject('error') 162 } else { 163 return response.data 164 } 165 }, 166 error => { 167 console.log('err' + error) 168 let { message } = error 169 if (message == 'Network Error') { 170 message = '后端接口连接异常' 171 } else if (message.includes('timeout')) { 172 message = '系统接口请求超时' 173 } else if (message.includes('Request failed with status code')) { 174 message = '系统接口' + message.substr(message.length - 3) + '异常' 175 } 176 Message({ 177 message: message, 178 type: 'error', 179 duration: 5 * 1000 180 }) 181 return Promise.reject(error) 182 } 183 )
spring boot 端 核心代码 加解密工具类,请见前面的贴子
DecodeRequestBodyAdvice.java
1 package com.ruoyi.framework.interceptor; 2 3 import com.ruoyi.common.constant.Constants; 4 import com.ruoyi.common.utils.enDeCrypt.RsaUtil; 5 import com.ruoyi.common.utils.enDeCrypt.TripleDesUtil; 6 import com.ruoyi.common.utils.enDeCrypt.UrlCode; 7 import lombok.SneakyThrows; 8 import org.springframework.core.MethodParameter; 9 import org.springframework.http.HttpHeaders; 10 import org.springframework.http.HttpInputMessage; 11 import org.springframework.http.converter.HttpMessageConverter; 12 import org.springframework.web.bind.annotation.ControllerAdvice; 13 import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice; 14 15 import java.io.*; 16 import java.lang.reflect.Type; 17 import java.nio.charset.StandardCharsets; 18 19 /** 20 * @author x8023z 21 */ 22 @ControllerAdvice(basePackages = "com.ruoyi.web.controller") 23 public class DecodeRequestBodyAdvice implements RequestBodyAdvice { 24 @Override 25 public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { 26 //true开启功能,false关闭这个功能 27 return true; 28 } 29 30 @Override 31 public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { 32 return body; 33 } 34 35 //在读取请求之前处理 36 @SneakyThrows 37 @Override 38 public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException { 39 if (httpInputMessage.getBody().available() <= 0) { 40 return httpInputMessage; 41 } 42 String SecurityKey = httpInputMessage.getHeaders().get("SecurityKey").get(0); 43 //System.out.println("SecurityKey:"+SecurityKey); 44 String Authentication = httpInputMessage.getHeaders().get("Authentication").get(0); 45 // System.out.println("Authentication:"+Authentication); 46 47 byte[] requestDataByte = new byte[httpInputMessage.getBody().available()]; 48 httpInputMessage.getBody().read(requestDataByte); 49 byte[] requestDataByteNew = null; 50 try { 51 // 解密 52 requestDataByteNew = this.decryptBody(requestDataByte, SecurityKey, Authentication); 53 } catch (IOException e) { 54 throw new RuntimeException("解密异常"); 55 } 56 // 使用解密后的数据,构造新的读取流 57 InputStream rawInputStream = new ByteArrayInputStream(requestDataByteNew); 58 return new HttpInputMessage() { 59 @Override 60 public HttpHeaders getHeaders() { 61 return httpInputMessage.getHeaders(); 62 } 63 64 @Override 65 public InputStream getBody() throws IOException { 66 return rawInputStream; 67 } 68 }; 69 } 70 71 72 /** 73 * 在Http消息转换器执转换,之后执行 74 * body 转换后的对象 75 * inputMessage 客户端的请求数据 76 * parameter handler方法的参数类型 77 * targetType handler方法的参数类型 78 * converterType 使用的Http消息转换器类类型 79 * 80 * @return 返回一个新的对象 81 */ 82 @Override 83 public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { 84 return body; 85 } 86 87 88 /** 89 * SHA256签名验证,DES3解密 90 * 91 * @param requestBytes 92 * @return 93 */ 94 public byte[] decryptBody(byte[] requestBytes, String SecurityKey, String Authentication) throws Exception { 95 byte[] byteResult = new byte[0]; 96 if (requestBytes.length <= 0) { 97 return byteResult; 98 } 99 100 //1. urldecode 得到base64 body 101 String requestData = new String(requestBytes, StandardCharsets.UTF_8); 102 UrlCode urlCode = new UrlCode(); 103 String urlDecodeData = urlCode.decodeURL(requestData, StandardCharsets.UTF_8).trim(); 104 // System.out.println("密文body:"+urlDecodeData); 105 106 RsaUtil rsaUtil = null; 107 String des3Key = ""; 108 109 rsaUtil = new RsaUtil(true, Constants.RSA_CBC_PKCS7, Constants.SHA256WITHRSA); 110 boolean isVerifyPassed = rsaUtil.verifySignPublicKey(urlDecodeData, Authentication, Constants.PUBLICKEY_C, true, true, false); 111 //System.out.println("isVerifyPassed:"+isVerifyPassed); 112 113 //2.先sha256withrsa 验证签名再解密密钥,再des3解密数据 114 String plainBody = ""; 115 if (isVerifyPassed) { 116 rsaUtil = new RsaUtil(true); 117 //3.解密des3key rsa RSA_PKCS1_PADDING 解密 118 des3Key = rsaUtil.decryptByPrivateKey(SecurityKey, Constants.PRRIVATEKEY_S, true, true, false); 119 // System.out.println("des3Key:"+des3Key); 120 if (des3Key != null) { 121 //4. des3解密body 122 TripleDesUtil tripleDes = new TripleDesUtil(urlDecodeData, Constants.DESEDE, des3Key, Constants.DES3_KEY_SIZE, Constants.DES3_IV, Constants.DES3_CBC_PKCS7, Constants.UTF8, true, true); 123 plainBody = tripleDes.decode(); 124 // System.out.println("plainBody:"+plainBody); 125 //验证通过,返回解密后的参数 126 return plainBody.getBytes(StandardCharsets.UTF_8); 127 } else { 128 //解密des3key异常 129 throw new RuntimeException("解密密钥异常"); 130 } 131 } else { 132 throw new RuntimeException("验签异常"); 133 //验签异常 134 } 135 } 136 }
EncodeResponseBodyAdvice.java
1 package com.ruoyi.framework.interceptor; 2 3 import com.alibaba.fastjson.JSON; 4 import com.alibaba.fastjson.serializer.SerializerFeature; 5 import com.ruoyi.common.constant.Constants; 6 import com.ruoyi.common.utils.enDeCrypt.AesUtil; 7 import com.ruoyi.common.utils.enDeCrypt.RsaUtil; 8 import lombok.SneakyThrows; 9 import org.springframework.core.MethodParameter; 10 import org.springframework.http.MediaType; 11 import org.springframework.http.server.ServerHttpRequest; 12 import org.springframework.http.server.ServerHttpResponse; 13 import org.springframework.web.bind.annotation.ControllerAdvice; 14 import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; 15 import java.net.URLEncoder; 16 17 /** 18 * @author EvianZou 19 */ 20 @ControllerAdvice(basePackages = "com.ruoyi.web.controller") 21 public class EncodeResponseBodyAdvice implements ResponseBodyAdvice { 22 23 @Override 24 public boolean supports(MethodParameter methodParameter, Class aClass) { 25 return true; 26 } 27 28 @SneakyThrows 29 @Override 30 public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { 31 String body = JSON.toJSONString(o, SerializerFeature.DisableCircularReferenceDetect); 32 33 RsaUtil rsaUtil = new RsaUtil(true); 34 AesUtil aesUtil = new AesUtil(); 35 36 // 密钥:动态生成AESKey,RSA客户端公钥加密,再base64 37 rsaUtil.setSignAlgorithm(Constants.SHA1WITHRSA); 38 rsaUtil.setPaddingMode(Constants.RSA_ECB_PKCS1); 39 40 String aesKey = aesUtil.getAesSecretKey(Constants.AES, Constants.PASSWORD, true); 41 String securityKey = rsaUtil.encryptByPublicKey(aesKey, Constants.PUBLICKEY_C, true, true, false); 42 // System.out.println("通过客户端公钥rsa加密密钥得到密文key:" + securityKey); 43 44 // 服务器端作为发送方 45 // 签名 传输数据转json字符串,base64,MD5摘要,RSA服务器端私钥生成签名,base64 46 rsaUtil.setSignAlgorithm(Constants.MD5WITHRSA); 47 rsaUtil.setPaddingMode(Constants.RSA_ECB_PKCS7); 48 49 // 密钥、密文 是否utf8,是否base64,是否hex 都影响前后端加解密是否一致。jdk1.8以下版本默认不支持aes密钥长度为256 50 String cipherData = aesUtil.encode(body, aesKey, Constants.AES_CBC_PKCS7, Constants.AES_IV, true, true, false); 51 // System.out.println("通过明文key加密body得到密文body:"+cipherData); 52 53 /* 54 //String plainText = aesUtil.decode(cipherText,aesKey,Constants.AES_CBC_PKCS7,Constants.AES_IV,true,true,false); 55 //System.out.println("通过明文key解密body得到明文body:" + plainText); 56 57 // aesKey="1234567890123456"; 58 // aesKey = DatatypeConverter.printBase64Binary(aesKey.getBytes(StandardCharsets.UTF_8)); 59 // aesKey = "8A5EzyHtJjklPj4a9NWwXw=="; 60 // System.out.println("测试,得到aesKey:" + aesKey); 61 62 // String test = aesUtil.encode("邹依芸 abcdef 123",aesKey,Constants.AES_CBC_PKCS7,Constants.AES_IV,true,true,false); 63 // System.out.println("测试,加密得到密文数据:" + test); 64 65 // test = aesUtil.decode(test,aesKey,Constants.AES_CBC_PKCS7,Constants.AES_IV,true,true,false); 66 // System.out.println("测试,解密得到明文数据:" + test); 67 */ 68 69 String authentication = rsaUtil.signByPrivateKey(cipherData, Constants.PRRIVATEKEY_S, true, true, false); 70 // System.out.println("通过服务器端的私钥key生成摘要得到签名" + authentication); 71 72 serverHttpResponse.getHeaders().set("Access-Control-Expose-Headers", "Authentication,SecurityKey"); 73 74 // 密钥:动态生成AESKey,RSA客户端公钥加密,再base64 75 serverHttpResponse.getHeaders().set("SecurityKey", securityKey); 76 77 // 服务器端作为发送方 78 // 签名 传输数据转json字符串,base64,MD5摘要,RSA服务器端私钥生成签名 79 serverHttpResponse.getHeaders().set("Authentication", authentication); 80 81 // 不直接返回加密字符串,因为控制层使用了@ResponseBody注解,直接返回字符串会在前后各多出一个双引号 82 83 // 解决数据传输丢失特殊符号问题:如url特殊字符, 丢失+号,+号变成了空格 84 return URLEncoder.encode(cipherData,Constants.UTF8); 85 } 86 87 }
三、原理
1、springboot: 切面
RequestBodyAdvice 可以理解为在 @RequestBody 之前需要进行操作,ResponseBodyAdvice 可以理解为在 @ResponseBody 之后进行的操作,所以当接口需要加解密时,在使用 @RequestBody 接收前台参数之前可以先在 RequestBodyAdvice 的实现类中进行参数的解密,当操作结束需要返回数据时,可以在@ResponseBody之后进入ResponseBodyAdvice的实现类中进行参数的加密。
2、Vue中使用axios封装的全局拦截器,拦截器(英文:Interceptors)会在每次发起 ajax 请求和得到响应的时候自动被触发。
3、加解密逻辑说明