前端API交互AES与RSA混合加密及在微信小程序中的使用
为了保证前端调用API的数据安全性,团队参考一些技术文档,选择使用AES和RSA对数据进行混合加密,保证数据安全性。主要思路如下:
前后端分别生成各自的RSA秘钥对(公钥、私钥),然后互相交换公钥。
前端发出请求时,在统一拦截器中做请求拦截,首先生成一个随机key(这里用的16位),然后用这个明文key对参数做AES加密,再用后端的公钥对这个key加密,此时可以得到一个加密的key和加密的参数,根据实际情况确定key是放在header还是也作为参数传给后端。
后端收到后,用自己的私钥对key进行解密,然后再用解密后的key解密传来的参数得到明文参数。后端返回数据时也会随机生成一个16位随机key,然后用这个明文key对返回数据做AES加密,再用前端给的公钥对这个key加密,最后把加密key和加密数据返回给前端。
前端此时会收到这个加密数据,在响应拦截里做统一处理,首先拿到加密的key,用自己的私钥进行解密,然后用明文key对加密数据进行解密,拿到明文数据。
这里主要做前端代码演示:
AES对称加密我们采用CryptoJS
,是一个标准和安全加密算法的JavaScript库,它的AES加密支持AES-128、AES-192和AES-256。下载或查看详情介绍请戳官网地址
GitHub地址:https://github.com/brix/crypto-js
官网地址:https://code.google.com/archive/p/crypto-js/
RSA非对称加密我们采用JSEncrypt
,它是一个很好用的RSA加密算法的JavaScript库,使用PKCS#1进行填充,加解密使用方式很简单,具体的介绍或者下载请移步官网
GitHub地址:https://github.com/travist/jsencrypt
官网地址:http://travistidwell.com/jsencrypt/
安装这两个库
npm install crypto-js
npm install jsencrypt
新建一个encryption.js
文件,里面写下这两个库的工具函数
import CryptoJS from 'crypto-js'
import { JSEncrypt } from 'jsencrypt'
/**
* aes加密
*/
export const aesUtil = {
// 获取key
genKey: function(length = 16) {
let random = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let str = ''
for (let i = 0; i < length; i++) {
str = str + random.charAt(Math.random() * random.length)
}
return str
},
// 加密
encrypt: function(plaintext, key) {
if (plaintext instanceof Object) {
// JSON.stringify
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) === '[') {
// JSON.parse
decString = JSON.parse(decString)
}
return decString
}
}
/**
* rsa加密
*/
export const rsaUtil = {
// RSA 位数,这里要跟后端对应
bits: 1024,
// 当前JSEncrypted对象
thisKeyPair: {},
// 生成密钥对(公钥和私钥)
genKeyPair: function(bits = rsaUtil.bits) {
let genKeyPair = {}
rsaUtil.thisKeyPair = new JSEncrypt({
default_key_size: bits
})
// 这里项目使用的是静态秘钥,所以该方法在本地执行一次,获取到配对的公私钥保存下即可
// 获取私钥
genKeyPair.privateKey = rsaUtil.thisKeyPair.getPrivateKey()
// 获取公钥
genKeyPair.publicKey = rsaUtil.thisKeyPair.getPublicKey()
return genKeyPair
},
// 公钥加密
encrypt: function(plaintext, publicKey) {
// 由于秘钥已经生存一对保存在本地,该方法就在内部调用,生成setPublicKey方法
this.genKeyPair()
if (plaintext instanceof Object) {
// 1、JSON.stringify
plaintext = JSON.stringify(plaintext)
}
publicKey && rsaUtil.thisKeyPair.setPublicKey(publicKey)
return rsaUtil.thisKeyPair.encrypt(JSON.stringify(plaintext))
},
// 私钥解密
decrypt: function(ciphertext, privateKey) {
privateKey && rsaUtil.thisKeyPair.setPrivateKey(privateKey)
let decString = rsaUtil.thisKeyPair.decrypt(ciphertext)
if (decString.charAt(0) === '{' || decString.charAt(0) === '[') {
// JSON.parse
decString = JSON.parse(decString)
}
return decString
}
}
/**
* 本地rsa已经生成的秘钥,可以统一带-----BEGIN PUBLIC KEY-----/-----END PUBLIC KEY-----的前后缀,也可以不带
*/
// export const publicKey = `前端的公钥`
export const privateKey = `前端的私钥`
/**
* 服务端rsa生成的公钥
*/
export const servePublicKey = `后端的公钥`
前端项目请求使用的是axios库,同时为了防止转义导致不能正确解密,前后端都使用了URI转码。在你的实际请求和响应拦截代码文件中做如下更改
import { aesUtil, rsaUtil, servePublicKey, privateKey } from './encryption'
// ...其它代码
// 请求拦截
request.interceptors.request.use(
config => {
// ...其它代码
// 如果data参数存在,headers中添加aesKey参数,再对data加密
if (config.data) {
// 随机生成key
const genKey = aesUtil.genKey()
// 用服务端的公钥对key进行rsa加密
const aesKey = rsaUtil.encrypt(genKey, servePublicKey)
// 这里后端让把key放在headers中,加密后使用encodeURIComponent进行了转码,小程序里是header不带s
config.headers['aesKey'] = encodeURIComponent(aesKey)
// 对传的数据data进行aes加密
const aesData = aesUtil.encrypt(config.data, genKey)
// 请求data修改为加密且转码后的data字符串
config.data = encodeURIComponent(aesData)
}
return config
},
error => {
console.error('request error', error)
return Promise.reject(error)
}
)
// 响应拦截
request.interceptors.response.use(
res => {
if (res.status === 200) {
// ...其它代码
// 获取headers里的aeskey解码后解密
// 注意:由于底层问题,后端在header里返回的字段名微信小程序里保留大写,Chrome里转成了小写
const key = rsaUtil.decrypt(decodeURIComponent(res.header['aes-key']), privateKey)
// 把数据解码后用解密后的key解密,拿到正确数据
const serveData = aesUtil.decrypt(decodeURIComponent(res.data), key)
const { code, message = 'Error', data } = serveData
if (code === 200) {
return Promise.resolve(data)
} else if () {
// ...其它代码
}
} else {
let message = '请求异常!'
showMessage('error', message)
return Promise.reject({ message: message, data: res.data })
}
},
error => {
console.error('response error', error)
showMessage('error', error.message)
return Promise.reject({ message: error.message, data: error })
}
)
这里的代码仅供参考,用于理解思路,这里说下不能正确解码常见的坑:
- 请审查下前后端的加密、填充方式是否一致、加密位数是否一致;
- 可以让后端生成两份秘钥对,一份给前端使用,前端不用自己生成的;
- 参数是否在传输过程中被转义了,建议用
encodeURIComponent
和decodeURIComponent
进行转解码,一定要告知后端。
还有一个问题会导致拿不到后端返回的key,由于本项目中后端返回的key是放在headers中,且字段名初始为aesKey
,为驼峰形式,但在Chrome浏览器中调试时,拿到的请求拦截配置config
中的字段名全部被转为了小写。在调试微信小程序时拿到的config
里又是没被转小写的aesKey
,这里要注意下。
在微信小程序中的使用
按照上面步骤在微信小程序中使用后,微信小程序写在对应的请求响应和拦截中,会发现报错appName
的问题。这是由于用npm安装的jsencrypt.js
代码里面含有window
、document
、navigator
对象,这些对象可以在pc端的浏览器使用,但是小程序没有这些对象。这里要对源码进行下修改,兼容微信小程序。
在node_modules/jsencrypt/bin/
中找到jsencrypt.js
文件,然后搜索对照下面注释的文件做修改:
// if (j_lm && (navigator.appName == "Microsoft Internet Explorer")) {
// BigInteger.prototype.am = am2;
// dbits = 30;
// }
// else if (j_lm && (navigator.appName != "Netscape")) {
// BigInteger.prototype.am = am1;
// dbits = 26;
// }
// else { // Mozilla/Netscape seems to prefer am3
// BigInteger.prototype.am = am3;
// dbits = 28;
// }
// 兼容小程序,只保留这部分
BigInteger.prototype.am = am3;
dbits = 28;
// if (window.crypto && window.crypto.getRandomValues) {
// // Extract entropy (2048 bits) from RNG if available
// var z = new Uint32Array(256);
// window.crypto.getRandomValues(z);
// for (t = 0; t < z.length; ++t) {
// rng_pool[rng_pptr++] = z[t] & 255;
// }
// }
// 兼容小程序的写法
var getRandomValues = function (array) {
for (var i = 0, l = array.length; i < l; i++) {
array[i] = Math.floor(Math.random() * 256);
} return array;
}
var z = new Uint32Array(256);
getRandomValues(z);
// 兼容小程序,删掉,绑定的事件对加解密没影响
// Use mouse events for entropy, if we do not have enough entropy by the time
// we need it, entropy will be generated by Math.random.
// var onMouseMoveListener_1 = function (ev) {
// this.count = this.count || 0;
// if (this.count >= 256 || rng_pptr >= rng_psize) {
// if (window.removeEventListener) {
// window.removeEventListener("mousemove", onMouseMoveListener_1, false);
// }
// else if (window.detachEvent) {
// window.detachEvent("onmousemove", onMouseMoveListener_1);
// }
// return;
// }
// try {
// var mouseCoordinates = ev.x + ev.y;
// rng_pool[rng_pptr++] = mouseCoordinates & 255;
// this.count += 1;
// }
// catch (e) {
// // Sometimes Firefox will deny permission to access event properties for some reason. Ignore.
// }
// };
// if (window.addEventListener) {
// window.addEventListener("mousemove", onMouseMoveListener_1, false);
// }
// else if (window.attachEvent) {
// window.attachEvent("onmousemove", onMouseMoveListener_1);
// }
// 兼容小程序,删掉
// window.JSEncrypt = JSEncrypt;
然后建议把这个文件单独复制出来,防止提交时忽略了node_modules,别的同事在使用时又重新下载依赖导致报错。
参考资料: