1. 项目背景(需求)
为了保证数据传输的安全性,利用AES+RSA混合加密,配合后端实现数据交互加密
项目环境:vue + axios
2. 实现过程(代码)
AES对称加密我们采用 CryptoJS,
AES加密支持AES-128、AES-192和AES-256 (AES传送门)
RSA非对称加密我们采用JSEncrypt
,(RSA传送门)
第一步:npm安装两个库
npm i crypto-js jsencrypt
第二步:新建encrypt.js,封装需要用的方法
import CryptoJS from 'crypto-js'
import { JSEncrypt } from 'jsencrypt'
/**
* 递归自然排序: key + value
* sortObjFunc: 排序方法
* isArraysFunc: 判断是否array
* isObjectFunc: 判断是否object
* isHasValues: 判断空值,null,undefined
* 排序前: {"aaa":"111","bbb":"222","list_1":[],"list":["3","13"],"map":{"b":"2","c":"3"}}
* 排序后: aaa111bbb222list423.852313list_1mapb2c3
*/
export const signUtil = {
sortObjFunc: function (plaintext) {
let signStr = ''
let keyList = []
for (const key in plaintext) { keyList.push(key) }
if (keyList.length) { keyList.sort() }
const len = keyList.length
for (let i= 0; i < len; i++) {
let value = plaintext[keyList[i]]
if (value && signUtil.isHasValues(value)) {
if (signUtil.isArraysFunc(value) || signUtil.isObjectFunc(value)) {
value = signUtil.sortObjFunc(value)
}
}
// 数组取value,否则取key+value(排除null,空,undefined)
if (signUtil.isArraysFunc(plaintext)) {
signStr += value
} else {
value !== null ? signStr += keyList[i] + value : signStr += keyList[i]
}
}
return signStr
},
isArraysFunc (item) { return Object.prototype.toString.call(item) === '[object Array]' },
isObjectFunc (item) { return Object.prototype.toString.call(item) === '[object Object]' },
isHasValues (item) { return item !== 'null' || item !== 'undefined' || item !== 0 }
}
/**
* aes加密
* genKey: 获取key
* encrypt: AES加密
* decrypt: AES解密
*/
export const aesUtil = {
genKey: function(expect = 16) {
const random = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let str = ''
while (str.length < expect) { str += random.charAt(Math.random() * random.length) }
return str
},
encrypt: function(plaintext, key) {
if (plaintext instanceof Object) { plaintext = JSON.stringify(plaintext) }
let encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(plaintext), CryptoJS.enc.Utf8.parse(key), {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.toString()
},
decrypt: function(ciphertext, key) {
let decrypt = CryptoJS.AES.decrypt(ciphertext, CryptoJS.enc.Utf8.parse(key), {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
let decString = CryptoJS.enc.Utf8.stringify(decrypt).toString()
if (decString.charAt(0) === '{' || decString.charAt(0) === '[') {
decString = JSON.parse(decString)
}
return decString
}
}
/**
* rsa加密
* encrypt: 公钥加密
* decrypt: 私钥解密
* ensign: rsa签名
* design: rsa验签
*/
const encryptor = new JSEncrypt({ default_key_size: 1024 })
export const rsaUtil = {
encrypt: function (key, publicKey) {
publicKey && encryptor.setPublicKey(publicKey)
return encryptor.encrypt(key)
},
decrypt: function (key, privateKey) {
privateKey && encryptor.setPrivateKey(privateKey)
return encryptor.decrypt(key)
},
ensign: function (data, privateKey){
privateKey && encryptor.setPrivateKey(privateKey)
return encryptor.sign(data, CryptoJS.SHA256, 'sha256')
},
design: function (data, signature, publicKey){
if (signature && publicKey) {
encryptor.setPrivateKey(publicKey)
return encryptor.verify(data, signature, CryptoJS.SHA256)
}
}
}
/**
* RSA秘钥对
* publicKey: 前端rsa公钥
* privateKey: 前端rsa私钥
* servePublicKey: 服务端rsa公钥
*/
export const publicKey = encryptor.getPublicKey()
export const privateKey = encryptor.getPrivateKey()
export const servePublicKey = '服务端公钥'
第三步:改造Axios
import axios from 'axios'
import { signUtil, aesUtil, rsaUtil, servePublicKey, privateKey } from 'encryptn'
axios.defaults.timeout = 20000;
axios.defaults.baseURL = 'http://8.8.8.8:8080'
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8';
axios.defaults.headers.post['Access-Control-Allow-Origin'] = '*';
// 请求拦截器
axios.interceptors.request.use((config) => {
// other code here...
if (config.data) {
// 生成签名
const signKey = signUtil(config.data)
const newSignKey = rsaUtil.ensign(signKey, privateKey)
// AES随机秘钥
const romkey = aesUtil.genKey()
// 服务端公钥对随机秘加密
const aesKey = rsaUtil.encrypt(romkey, servePublicKey)
// AES + 随机秘对data体加密
config.data['sign'] = newSignKey
const aesData = aesUtil.encrypt(config.data, romkey)
const newData = { data: aesData, key: aesKey }
config.data = newData
}
return config;
}, (error) => {
return Promise.reject(error);
});
// 响应拦截器
axios.interceptors.response.use((res) => {
// other code here
const { data, key } = res.data
// 客户端私钥解密key
const rsaKey = rsaUtil.decrypt(key, privateKey)
// AES + key解密data
const newData = aesUtil.decrypt(data, rsaKey)
// 重新生成签名验证
if (newData.sign) {
const copyData = JSON.parse(JSON.stringify(newData))
delete copyData.sign
const data = signUtil(copyData)
const flag = rsaUtil.design(data, newData.sign, servePublicKey)
if (!flag) { return Promise.reject({ message: '签名失败' }) }
return Promise.resolve(res.data)
} else {
return Promise.reject({ message: '请求异常' })
}
}, (error) => {
return Promise.reject(error);
});
## 3. 注意问题
- 签名排序统一,需要和后端签名一致才可以通过
- 加密长度统一,这里统一使用1024,具体前后端协商
- 数据结构统一,返回体的数据结构一致,方便后面验证签名
- 前端加密一定程度增加了网络攻击的难度,最好还是上https