前端API交互AES与RSA混合加密及在微信小程序中的使用

前端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 })
    }
)

这里的代码仅供参考,用于理解思路,这里说下不能正确解码常见的坑:

  • 请审查下前后端的加密、填充方式是否一致、加密位数是否一致;
  • 可以让后端生成两份秘钥对,一份给前端使用,前端不用自己生成的;
  • 参数是否在传输过程中被转义了,建议用encodeURIComponentdecodeURIComponent进行转解码,一定要告知后端。

还有一个问题会导致拿不到后端返回的key,由于本项目中后端返回的key是放在headers中,且字段名初始为aesKey,为驼峰形式,但在Chrome浏览器中调试时,拿到的请求拦截配置config中的字段名全部被转为了小写。在调试微信小程序时拿到的config里又是没被转小写的aesKey,这里要注意下。

在微信小程序中的使用

按照上面步骤在微信小程序中使用后,微信小程序写在对应的请求响应和拦截中,会发现报错appName的问题。这是由于用npm安装的jsencrypt.js代码里面含有windowdocumentnavigator对象,这些对象可以在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,别的同事在使用时又重新下载依赖导致报错。

参考资料:

前后端API交互数据加密——AES与RSA混合加密完整实例

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值