项目背景
1. 客户需要数据安全策略;
2. 所有数据接口具备反爬;
3. 敏感数据信息加密;
解决方案
接口加密只是一个策略,这个加密不像密码加密,必须要可逆,也就是,加密后可以解密出原数据。
基本加密流程如下:
1. 前端发送请求,需要对请求数据进行加密;
2. 后端收到请求,对请求进行解密;
3. 后端响应请求,需要对响应数据进行加密;
4. 前端收到响应,需要对响应数据进行解密;
从上述流程可以看出,我们得指定前后端通用的加密方式,使前后端可以解密各自加密后的数据才可以,而且这种加密方式还不能轻易的被解密,如果被轻易的解密,比如使用简单的base64进行编码,那这种也是毫无意义。
加解密算法:
对称加密:常见的有DES、AES、3DES、RC5、RC6,此类算法的优点:效率高、速度快,适合大量数据的加解密,缺点是密钥传输的过程不安全,且容易被破解。
非对称加密:它需要使用“一对”密钥来分别完成加密和解密操作,一个公开发布,即公开密钥,另一个由用户自己秘密保存,即私用密钥。信息发送者用公开密钥去加密,而信息接收者则用私用密钥去解密。公钥机制灵活,但加密和解密速度却比对称密钥加密慢得多。
非对称密钥加密的使用过程:
1. A要向B发送信息,A和B都要产生一对用于加密和解密的公钥和私钥。
2. A的私钥保密,A的公钥告诉B;B的私钥保密,B的公钥告诉A。
3. A要给B发送信息时,A用B的公钥加密信息,因为A知道B的公钥。
4. A将这个消息发给B(已经用B的公钥加密消息)。
5. B收到这个消息后,B用自己的私钥解密A的消息,其他所有收到这个报文的人都无法解密,因为只有B才有B的私钥。
6. 反过来,B向A发送消息也是一样。
从上面大家应该可以看出对称加密和非对称加密的区别,下面稍微进行一下总结:
(1) 对称加密加密与解密使用的是同样的密钥,所以速度快,但由于需要将密钥在网络传输,所以安全性不高。
(2) 非对称加密使用了一对密钥,公钥与私钥,所以安全性高,但加密与解密速度慢。
(3) 解决的办法是将对称加密的密钥使用非对称加密的公钥进行加密,然后发送出去,接收方使用私钥进行解密得到对称加密的密钥,然后双方可以使用对称加密来进行沟通。
代码实现
上面我们已经探讨了,整个的实现流程,那我们就把上述流程应用到我们前后端的代码中:
首先,前后端自己生成用于加密和解密的公钥和私钥,前端的私钥保密,前端的公钥告诉后端;后端的私钥保密,后端的公钥告诉前端。
前端数据发送加密实现
首先,前端加密需要引入依赖
// aes 加密 npm install crypto-js //rsa 加密 npm install encryptlong
import CryptoJS from 'crypto-js' import { JSEncrypt } from 'encryptlong' const rsa= { // rsa front end mePublicKey: "xxx...", mePrivateKey:"ccc...", // rsa backend backPublicKey: "bbb..." } const aes= { iv: "xxxxxxxxxxxxxxxx" } //①随机生成对称加密密钥 export function generateKey(){ return CryptoJS.lib.WordArray.random(128/8).toString(); } // aes 加密 export function aesEncode(content,aesKey){ let iv = CryptoJS.enc.Utf8.parse(aes.iv); aesKey = CryptoJS.enc.Utf8.parse(aesKey); let encrypted = CryptoJS.AES.encrypt(content, aesKey, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); } // rsa 加密 export function rsaEncode(content){ const encrypt = new JSEncrypt(); encrypt.setPublicKey('-----BEGIN PUBLIC KEY-----' + rsa.backPublicKey + '-----END PUBLIC KEY-----'); return encrypt.encryptLong(content); }
加密调用流程,我们从前端请求 request进行拦截,这里测试使用 接口判断的形式,可以自己定义哪些接口需要加密
关键代码:
// request拦截器 service.interceptors.request.use(config => { // 是否需要设置 token const isToken = (config.headers || {}).isToken === false if (getToken() && !isToken) { config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 } // 判断接口是否需要加密 if(urls.urls.includes(config.url)){ // ①随机生成对称加密密钥 let key=generateKey() let data=JSON.stringify(config.data) // ②用生成的密钥对请求的内容进行加密 data=aesEncode(data,key) config.data={} config.data.data=data // ③加密密钥,将数据和密钥一块发送给后端 let encodeAesKey = rsaEncode(key); config.headers['x-encrypt-front-header'] = encodeAesKey } return config }, error => { console.log(error) Promise.reject(error) })
后端数据解密实现
采用 controller advice 对 请求进行加强处理,判断是否需要解密
/** * decrypt request data advice */ @ControllerAdvice @ConditionalOnProperty(prefix = "spring.crypto.request.decrypt", name = "enabled" , havingValue = "true", matchIfMissing = true) public class DecryptRequestBodyAdvice implements RequestBodyAdvice { @Autowired private KeyConfig keyConfig; /** * needed decrypt or not */ private boolean isDecode; @Override public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { isDecode= NeedCrypto.needDecrypt(methodParameter); return isDecode; } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException { if(isDecode){ // 需要解密,就进行解密流程 return new DecodeInputMessage(httpInputMessage, keyConfig); } return httpInputMessage; } @Override public Object afterBodyRead(Object obj, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { return obj; } @Override public Object handleEmptyBody(Object obj, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { return obj; } }
解密流程:
/** * decrypt post data info * @author dzq */ @Slf4j public class DecodeInputMessage implements HttpInputMessage { private HttpHeaders headers; private InputStream body; public DecodeInputMessage(HttpInputMessage httpInputMessage, KeyConfig keyConfig) { // get key from headers this.headers = httpInputMessage.getHeaders(); String encodeAesKey = ""; List<String> keys = this.headers.get("x-magic-front-header"); if (keys != null && keys.size() > 0) { encodeAesKey = keys.get(0); } try { // 1. decrypt key from encodeAesKey String decodeAesKey = RsaUtils.decodeBase64ByPrivate(keyConfig.getRsaPrivateKey(), encodeAesKey); // 2. get encrypted content from request body String encodeAesContent = new BufferedReader(new InputStreamReader(httpInputMessage.getBody())).lines().collect(Collectors.joining(System.lineSeparator())); encodeAesContent=JSONUtil.parseObj(encodeAesContent).get("data").toString(); // 3. decrypt request body info by CBC String aesDecode = AesUtils.decodeBase64(encodeAesContent, decodeAesKey, keyConfig.getAesIv().getBytes(), AesUtils.CIPHER_MODE_CBC_PKCS5PADDING); if (!StringUtils.isEmpty(aesDecode)) { // 4. reset decrypted request body this.body = new ByteArrayInputStream(aesDecode.getBytes()); } } catch (Exception e) { e.printStackTrace(); String err=" decrypt request body failed"+e.getMessage(); this.body=new ByteArrayInputStream(err.getBytes()); } } @Override public InputStream getBody() throws IOException { return body; } @Override public HttpHeaders getHeaders() { return headers; } }
后端数据加密
采用 controller advice 对 响应进行加强处理,判断是否需要加密
/** * encrypt response data advice * @author dzq */ @ControllerAdvice public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> { @Autowired private KeyConfig keyConfig; /** * needed encrypt or not */ private boolean isEncode; @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { // response data need encrypted or not by annotation @EncryptResponse isEncode= NeedCrypto.needEncrypt(returnType); return isEncode ; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if( !isEncode ){ return body; } if(!(body instanceof ApiResult)){ return body; } //only encrypt data of ApiResult ApiResult result = (ApiResult) body; Object data = result.getData(); if(null == data){ return body; } String returnData=JSONUtil.toJsonStr(data); try { // ①. 生成随机密钥 String randomAesKey = AesUtils.generateSecret(256); // ② 用生成的密钥加密数据 String aesEncode = AesUtils.encodeBase64(returnData, randomAesKey, keyConfig.getAesIv().getBytes(), AesUtils.CIPHER_MODE_CBC_PKCS5PADDING); result.setData(aesEncode); // ③ 加密密钥 String key=RsaUtils.encodeBase64PublicKey(keyConfig.getFrontRsaPublicKey(), randomAesKey); response.getHeaders().set("x-magic-header",key); }catch (Exception e){ // if throw exception, return msg to front end e.printStackTrace(); result.setData("data encrypt failed"+e.getMessage()); } return result; } }
前端数据解密实现
// aes decode export function aesDecode(encrypted,aesKey){ //console.log(aes.iv) let iv = CryptoJS.enc.Utf8.parse(aes.iv); aesKey = CryptoJS.enc.Utf8.parse(aesKey); var decrypted = CryptoJS.AES.decrypt(encrypted, aesKey, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // convert to utf8 string return CryptoJS.enc.Utf8.stringify(decrypted); } // rsa decode export function rsaDecode(content){ const encrypt = new JSEncrypt(); encrypt.setPrivateKey(rsa.mePrivateKey); return encrypt.decryptLong(content); }
前端需要拦截后端响应,进行解密
// 响应拦截器 service.interceptors.response.use(res => { // 未设置状态码则默认成功状态 const code = res.data.code || 200; // 获取错误信息 const msg = errorCode[code] || res.data.message || errorCode['default'] // 二进制数据则直接返回 if(res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer'){ return res.data } if (code === 401) { if (!isReloginShow) { isReloginShow = true; MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' } ).then(() => { isReloginShow = false; store.dispatch('LogOut').then(() => { // 如果是登录页面不需要重新加载 if (window.location.hash.indexOf("#/login") != 0) { location.href = '/index'; } }) }).catch(() => { isReloginShow = false; }); } return Promise.reject('无效的会话,或者会话已过期,请重新登录。') } else if (code === 500) { Message({ message: msg, type: 'error' }) return Promise.reject(new Error(msg)) } else if (code !== 200) { Notification.error({ title: msg }) return Promise.reject('error') } else { // 判断是否需要解密 let data=res.data; let key=res.headers['x-magic-header'] if(key&&key!=null){ // ① 解密密钥 key= rsaDecode(key) // ② 解密内容 data.data=JSON.parse(aesDecode(data.data,key)); } console.log(data,"请求数据") return data } }, error => { console.log('err' + error) let { message } = error; if (message == "Network Error") { message = "后端接口连接异常"; } else if (message.includes("timeout")) { message = "系统接口请求超时"; } else if (message.includes("Request failed with status code")) { message = "系统接口" + message.substr(message.length - 3) + "异常"; } Message({ message: message, type: 'error', duration: 5 * 1000 }) return Promise.reject(error) } )
详细代码可以参考:
后端代码:https://github.com/MagicDu/zhiyu
前端代码:https://github.com/MagicDu/zhiyu-ui
参考文章:Springboot2接口加解密全过程详解(含前端代码)