很久很久以前。。
很久很久以前,我们的项目前后端交互数据都是明文传输的。
直到有一天,有一个甲方来势汹汹的说到:你们这样很不安全啊~
在项目中添加使用 encryption 加密模块
与后端同事协商后,决定使用RSA加密方式来进行数据交互。并将 RSA 加密封装成独立的 vuex 仓库模块。
在项目中,我们可以根据环境不同来决定是否启用接口加密。并在路由拦截器中通过环境变量的判断来触发 vuex 的 encryption 加密模块。
Encryption 初始化加密流程
1. 在路由拦截器中触发 vuex action 方法 ENCRYPTION_INIT 进行加密初始化;
2. ENCRYPTION_INIT 方法中首先会从本地的 Cookie 中获取加密信息,看近期是否进行过加密初始化工作;
// ------- cookies/encryption.js --------------
// 获取 Secret
export function getSecret() {
const value = Cookies.get('encrytion-secret')
return value ? Base64.decode(value) : value
}
// 获取 Code
export function getCode() {
const value = Cookies.get('encrytion-code')
return value ? Base64.decode(value) : value
}
// 获取 Code 和 Secret
export function getSecretAndCode() {
const code = getCode()
const secret = getSecret()
return {
hasCookie: code && secret,
code,
secret
}
}
// ------- Encryption.js --------------
// 初始化AES的加密类
!state.secretAesEncryptor && commit('INIT_SECRET_AES_ENCRYPTOR')
const { hasCookie, code, secret } = getSecretAndCode()
if (hasCookie && !flag) { // cookie 存在的情况下直接存储参数
if (secret === state.secretAesEncryptor.encrypt(state.appSecret)) return
// 存储加密信息
commit('SET_APP_SECRET', state.secretAesEncryptor.decrypt(secret))
commit('SET_APP_CODE', state.secretAesEncryptor.decrypt(code))
return
}
3. 加密方式我们采用的是: AES 与 RSA 两个算法进行加密。第一步就是通过接口先获取服务端的 RSA公钥;
4. 通过 node-rsa 插件来生成客户端的 RSA密钥对;
// 生成密钥对
export function generateRsaKeys() {
return new Promise((r, j) => {
try {
const key = new NODERSA({ b: 1024 }) // 生成1024位的密钥
key.setOptions({ encryptionScheme: 'pkcs1' })
const publicDer = key.exportKey('pkcs8-public') // 公钥
const privateDer = key.exportKey('pkcs8-private') // 私钥
r({
PRIVATE_KEY: spliceKeyString(privateDer),
PUBLIC_KEY: spliceKeyString(publicDer)
})
} catch (error) {
j('密钥对生成失败' + error)
}
})
}
// ------- Encryption.js --------------
async ENCRYPTION_INIT({ dispatch, commit, state }, flag) {
// ...
try {
await dispatch('GET_SERVER_PUBLICKEY')
// 生成客户端密钥对
const { PRIVATE_KEY, PUBLIC_KEY } = await generateRsaKeys()
commit('SET_CLIENT_PUBLICKEY', PUBLIC_KEY)
commit('SET_CLIENT_PRIVATEKEY', PRIVATE_KEY)
// 初始化客户端RSA加密类
commit('INIT_CLIENT_RSA_ENCRYPTOR')
// ...
} catch (error) {
commit('SET_ENCRYPTION_STATUS', false)
console.log(error)
}
},
// 获取服务端公钥
async GET_SERVER_PUBLICKEY({ commit, dispatch }) {
try {
const { data: publicKey } = await getSeverPublicKey()
commit('SET_SERVER_PUBLICKEY', publicKey)
} catch (error) {
throw Error('system error: 获取服务端公钥失败')
}
},
5. 通过获取到的 服务端RSA公钥 来加密我们生成 客户端RSA公钥,并发送请求传递给后端;
6. 后端通过自己的 服务端RSA私钥 解密后就获取到我们的 客户端RSA公钥 了;
7. 后端会通过我们的 客户端RSA公钥 来加密一段密文并在上一次的接口中返回给我们;
8. 我们使用 服务端RSA私钥 解密并获取到真实的密文信息;
/**
* 与后端约定好传参形式
* 截取客户端公钥首尾字母,并用服务端公钥加密
* @param {string 客户端公钥} publicKey
* @returns Object<clientPubKey, enClientPubKey>
*/
function splitClientPublicKey(publicKey, serverRsaEncryptor) {
const keysArr = publicKey.split('')
const clientPubKey = keysArr.splice(1, keysArr.length - 2)
.join('')
return {
clientPubKey,
enClientPubKey: serverRsaEncryptor.encrypt(keysArr.join(''))
}
}
// ------------------------------
// 获取客户端加密的应用code和secret
async GET_ClIENT_SECRET({ state, commit, dispatch }) {
try {
// 用服务端公钥初始化服务端 RSA 加密类
!state.serverRsaEncryptor && commit('INIT_SERVER_RSA_ENCRYPTOR')
const {
serverPublicKey,
clientPublicKey,
serverRsaEncryptor,
clientRsaEncryptor
} = state
const { data } = await getClientSecret({
servicePubKey: serverPublicKey,
...splitClientPublicKey(clientPublicKey, serverRsaEncryptor)
})
const { s, c } = JSON.parse(clientRsaEncryptor.decrypt(data))
setCode(state.secretAesEncryptor.encrypt(c))
setSecret(state.secretAesEncryptor.encrypt(s))
commit('SET_APP_SECRET', s)
commit('SET_APP_CODE', c)
commit('SET_ENCRYPTION_STATUS', true)
} catch (error) {
console.log(error)
throw Error('system error: 获取服务端公钥失败', error)
}
}
9. 将密文直接存储到 vuex 仓库中以便后续发送请求时携带密文信息,同时再使用 AES算法 将密文加密后存储到 Cookie 中防止后续重复进行加密初始化工作;
RSA 非对称加密原理
从上面的流程中,已经能看出 RSA 非对称加密的原理了。
- 前后端分别生成了一对
RSA公钥、私钥;
- 并通过接口交换自己的
公钥;
- 在后续的请求交互中,分别使用对方的
公钥进行加密发送信息和自己的私钥进行解密获取信息
RSA/AES 加密类的封装
RsaEncryptor加/解密 构造类
// RsaEncryptor 加/解密 构造类
export class RsaEncryptor {
constructor(opt) {
const { PUBLIC_KEY, PRIVATE_KEY } = opt
this.JSEncrypt = new JSEncrypt()
this.JSDecrypt = new JSEncrypt()
this.JSDecrypt.setPrivateKey(PRIVATE_KEY)
this.JSEncrypt.setPublicKey(PUBLIC_KEY)
this.PRIVATE_KEY = PRIVATE_KEY
this.PUBLIC_KEY = PUBLIC_KEY
}
// 加密
encrypt(msg) {
if (!this.PUBLIC_KEY) throw Error('RsaEncryptor Error: missing PUBLIC_KEY')
return this.JSEncrypt.encrypt(msg)
}
// 解密
decrypt(data) {
if (!this.PRIVATE_KEY) throw Error('RsaEncryptor Error: missing PRIVATE_KEY')
return this.JSDecrypt.decrypt(data)
}
}
AesEncryptor加/解密 构造类
// AesEncryptor 加/解密 构造类
export class AesEncryptor {
constructor(key) {
if (!key) throw Error('AesEncryptor Error: missing key')
this.key = crypto.enc.Hex.parse(key)
}
// 加密
encrypt(msg) {
const srcs = crypto.enc.Utf8.parse(msg)
const encrypted = crypto.AES.encrypt(srcs, this.key, {
mode: crypto.mode.ECB,
padding: crypto.pad.Pkcs7
});
return encrypted.toString()
}
// 解密
decrypt(data) {
const decrypt = crypto.AES.decrypt(data, this.key, {
mode: crypto.mode.ECB,
padding: crypto.pad.Pkcs7
})
return crypto.enc.Utf8.stringify(decrypt).toString()
}
}