实际项目中为了系统安全,我们经常需要对请求数据和响应数据做加密处理,这里以spring后台,vue前台的java web为例,记录一个实现过程
项目背景
1. 客户需要数据安全策略;
2. 所有数据接口具备反爬;
3. 敏感数据信息加密;
一、为什么要结合AES和RSA?
因为AES是对称加密,即加密解密用的秘钥是一样,这样一来AES的秘钥保管尤其重要,但是AES有个很好的优点,就是处理效率高。而RSA是不对称加密,即加密解密用的秘钥不一样,分别叫公钥和私钥,通常用公钥加密,然后用私钥解密,其中公钥可以公开,只需要保管好私钥即可,而相比AES而言RSA速度慢效率低。所以,通常我们结合这两种加密方式的优点来完成数据的安全传输。
二、AES和RSA的结合使用过程
1.前端随机动态生成aesKey:因为AES的加密解密秘钥需要一致,如果整个系统写死AES的秘钥会很不安全,所以每次请求动态生成aesKey会比较好
2.前端用RSA对动态aesKey加密:动态aesKey需要传到后端供解密,传输过程用RSA加密
3.前端保存动态aesKey:因为同一个请求的响应需要一样的aesKey解密,所以前端还得把动态aesKey保存下来,可以再随机生成一个加密值,然后按键值对的方式保存在前端变量中{加密值: aesKey}
4.把加密的aesKey和id放到请求头
5.后端用RSA私钥解密得明文aesKey:后端从请求头取出加密的aesKey,然后用私钥解密拿到明文的aesKey,然后对请求数据解密
6.后端用明文aesKey加密响应数据
7.后端把请求过来的值放到响应头
8.前端根据响应头的值,取到对应的aesKey,对响应数据解密
9.前端删除动态的aesKey
三、具体实现案例
前端实现AES和RSA的公共方法
[JavaScript] 纯文本查看 复制代码 // aes 加密
npm install crypto-js
//rsa 加密
npm install encryptlong
[JavaScript] 纯文本查看 复制代码 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('----- KEY-----' + rsa.backPublicKey + '----- KEY-----');
return encrypt.encryptLong(content);
}
加密调用流程,我们从前端请求 request进行拦截,这里测试使用 接口判断的形式,可以自己定义哪些接口需要加密
关键代码:
[JavaScript] 纯文本查看 复制代码 // 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 对 请求进行加强处理,判断是否需要解密
[Java] 纯文本查看 复制代码 @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;
}
}
解密流程:
[Java] 纯文本查看 复制代码 @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 对 响应进行加强处理,判断是否需要加密
[Java] 纯文本查看 复制代码 @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;
}
}
前端数据解密实现
[JavaScript] 纯文本查看 复制代码 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);
}
前端需要拦截后端响应,进行解密
[JavaScript] 纯文本查看 复制代码 // 响应拦截器
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)
}
) |