项目背景
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接口加解密全过程详解(含前端代码)
