更新记录

1.0.0(2020-05-21)

项目初建


平台兼容性

项目相关

使用场景

  • 如果只是为了防止用户数据泄露,有条件用https,那不要犹豫,赶快买个证书。
  • 但是https也有局限性,加密层位于http层(应用层)和tcp层(传输层)之间, 所以抓到的http层的数据并没有加密。

单独加密的弊端

  • 单独用RSA非对称加密的话,客户端解密的时候需要用到私匙,这样无异于裸奔,使得整个加密毫无意义,除非你客户端只加密不解密,服务器直接返回明文,但这样就不是双向加密了
  • 单独用AES对称加密的话,加密解密用同一个密匙,密匙就在客户端放着,也是裸奔

AES + RSA 加密思路

  • 在启动APP时,本地随机生成AES密匙,不做持久化存储。
  • 通信的时候,将AES密匙通过RSA加密发送给服务器,将通信内容用过AES加密发送给服务器,这样服务器通过RSA解密得到AES密匙,再通过AES解密得到通信明文内容。
  • 返回数据的时候,服务器通过AES加密返回密文,客户端用过AES解密得到明文。这样,抓包是无法获取AES密匙的,AES密匙只存在于本地内存中
  • 总结来说,就是通过AES加密通信内容,RSA加密AES密匙

目录结构

  • js_sdk/encryption/crypto-js // AES加密库
  • js_sdk/encryption/jsencrypt // RSA加密库
  • js_sdk/encryption/utils.js // 封装好的 AES, RSA加密方法

使用方法(这里只是提供一种参考方法)

  1. 导入插件
  2. Vuex的modules模块里,初始化AES密匙,存做全局变量,例如我创建了 /store/modules/app.js 模块(对Vuex不甚明白的,看Vuex官方文档
    import { initAesKey, rsaEncrypt } from '@/js_sdk/encryption/utils';
    const state = {
    publicKey: null, // RSA加密公匙
    aesKey: null, // AES加密密匙
    aesEncryptKey: null, // AES加密密匙的RSA加密字符串
    }
    const mutations = {
    SET_PUBLICKEY: (state, publicKey) => {
        state.publicKey = publicKey
    },
    SET_AESKEY: (state, aesKey) => {
        state.aesKey = aesKey
    },
    SET_AESENCRYPTKEY: (state, aesEncryptKey) => {
        state.aesEncryptKey = aesEncryptKey
    },
    }
    const actions = {
    // 初始化请求,获取RSA公匙
    async initPublicKey({
        commit
    }) {
        // 这里的AppApi是我事先封装好的网络请求,用来向服务器获取公匙的,你要替换成你的网络请求
        const res = await AppApi.getPublicKey()
        if (res.code) {
            // 存下RSA公匙
            commit('SET_PUBLICKEY', res.data.key)
            // 随机生成AES密匙,并存下来
            commit('SET_AESKEY', initAesKey())
            // 将AES密匙进行RSA加密,并存下来
            commit('SET_AESENCRYPTKEY', rsaEncrypt(state.aesKey, state.publicKey))
        }
        return res;
    }
    }
    export default {
    namespaced: true,
    state,
    mutations,
    actions
    }
  3. 在main.js中调用app.js里的initPublicKey方法
    // 初始化通信公匙
    store.dispatch('app/initPublicKey').then((res) => {
    // 获取公匙完成,开始加载网页
    app.$mount()
    })
  4. 完成2,3两步,现在你的APP在启动之时,即初始化了publicKey,aesKey,aesEncryptKey三个变量,接着我们来到http拦截器(如果你没有用到http拦截器,强烈建议你去插件市场搜索一款)
  5. 这里是我的http拦截器代码
    import { aesEncrypt, aesDecrypt } from '@/js_sdk/encryption/utils';
    /**
    * 全局配置
    * 只能配置 静态数据
    * `content-type` 默认为 application/json
    * `header` 中`content-type`设置特殊参数 或 配置其他会导致触发 跨域 问题,出现跨域会直接进入响应拦截器的catch函数中
    */
    export const config = {
    header: {
        // 这里要注意,请求内容设置为 "text/plain",不要是其他的例如键值对类型
        contentType: "text/plain"
    }
    }
    /**
    * 全局 请求拦截器, 支持添加多个拦截器
    * 例如: 配置token、添加一些默认的参数
    *
    * `return config` 继续发送请求
    * `return false` 会停止发送请求,不会进入错误数据拦截,也不会进入请求对象中的catch函数中
    * `return Promise.reject('xxxxx')` 停止发送请求, 会错误数据拦截,也会进入catch函数中
    *
    * @param {Object} config 发送请求的配置数据
    */
    globalInterceptor.request.use(
    config => {
        if(Vue.prototype.$store.getters.aesEncryptKey) {
            // 将AES密匙(RSA加密后)放入请求头中
            config['header']['aes-key'] = Vue.prototype.$store.getters.aesEncryptKey;
            if(!config['data']) {
                config['data'] = {};
            }
            // 将时间戳放入请求内容里面
            config['data']['timetoken'] = Vue.prototype.$store.getters.timestamp;
            // 将请求内容字符串化,进行AES加密
            let data = aesEncrypt(JSON.stringify(config['data']), Vue.prototype.$store.getters.aesKey);
            // 将请求内容替换成加密后的字符串,如果你的请求内容是键值对类型,这里会出错的
            config['data'] = data;
        }
        return config;
    },
    err => {
        console.error("is global fail request interceptor: ", err);
        return false;
    }
    );
    /**
    * 全局 响应拦截器, 支持添加多个拦截器
    * 例如: 根据状态码选择性拦截、过滤转换数据
    *
    * `return res` 继续返回数据
    * `return false` 停止返回数据,不会进入错误数据拦截,也不会进入catch函数中
    * `return Promise.reject('xxxxx')` 返回错误信息, 会错误数据拦截,也会进入catch函数中
    *
    * @param {Object} res 请求返回的数据
    * @param {Object} config 发送请求的配置数据
    * @return {Object|Boolean|Promise<reject>}
    */
    globalInterceptor.response.use(
    (res, config) => {
        if (res.statusCode == 200) {
            const code = parseInt(res.data.code);
            if(code) {
                // 如果有code,说明返回的是明文,这种请求也就是在初始化获取RSA公匙的那一次才会有
                return res.data;
            }else{
                // AES解密
                let content = aesDecrypt(res.data, Vue.prototype.$store.getters.aesKey);
                // 明文json化,返回
                return JSON.parse(content)
            }
        } else {
            uni.showModal({
                title: "请求错误",
                showCancel: false,
                content: "errMsg:" + res.errMsg + "\nstatusCode:" + res.statusCode
            })
            return Promise.reject(res, config);
        }
    },
    (err, config) => {
        // 请求错误, 有可能服务器没开, 有可能是跨域问题, 情况非常多, 不可能一一进行逻辑处理, 只能弹窗提示
        uni.showModal({
            title: "请求异常",
            content: err.errMsg,
            showCancel: false
        })
        return Promise.reject(err);
    }
    );
  6. 以上就是js端的代码,仅作参考,请根据自己代码进行调整
  7. 后端代码,这里暂时只提供php代码作为参考,以下是thinkphp6.0框架的模块中间件代码
    <?php
    namespace app\api\middleware;
    use app\common\model\db\WebConfig;
    use app\Request;
    class Encryption
    {
    /**
     * 处理请求
     * @param $request
     * @param \Closure $next
     * @return mixed|\think\response\Json
     */
    public function handle(Request $request, \Closure $next)
    {
        $aesKey = $request->getAesKey();
        if ($aesKey && $aesKey != 'null') {
            // 获取RSA私匙
            $privateKey = WebConfig::getRSAPrivateKey();
            // $encrypted为需要解密的数据(如果加密的时候用了base64,这里则需要解码),$decrypted为解密后的数据,$privateKey同上,为私钥密钥
            openssl_private_decrypt(base64_decode($aesKey), $decrypted, $privateKey);
            // 解密之后的字符串 $decrypted ,为aes密匙
            $request->aesKey = $decrypted;
            $content = $request->getContent();
            // aes解密
            $_content = aesDecrypt($content, $decrypted);
            // 转json
            $contentArr = json_decode(trim($_content), true);
            if ($contentArr) {
                foreach ($contentArr as $k => $v) {
                    $request->$k = $v;
                }
            }
            if ($request->timetoken) {
                // 这里的时间,请自己根据实际情况做调整,毕竟timetoken这是客户端时间,客户端时间很有可能不是准的,我是在客户端弄了socket连接通过心跳来校准时间
                if (time() - $request->timetoken > 10 || time() - $request->timetoken < -5) {
                    // 时间异常
                    return json(config(TimeException));
                }
            } else {
                return json(config(TimeException));
            }
            return $next($request);
        } else {
            // 没有AES密匙,要么非法,要么当前是 getPublicKey 方法
            $pathinfo = $request->pathinfo();
            $urls = explode('/', $pathinfo);
            $controller = $urls[0];
            $action = $urls[1];
            if ($controller == 'web' && $action == 'getPublicKey') {
                // 如果当前是在行获取公匙的路由,那么可以放行
                return $next($request);
            } else {
                return json(config(AESNo));
            }
        }
    }
    }
  8. php的RES加密解密方法如下
    /**
     * AES解密
     * @param $content
     * @param $key
     * @return false|string
     */
    function aesDecrypt($content, $key)
    {
        return openssl_decrypt($content, 'AES-256-CBC', $key, OPENSSL_ZERO_PADDING, 'ZZWBKJ_ZHIHUAWEI');
    }
    /**
     * AES加密
     * @param $content
     * @param $key
     * @return false|string
     */
    function aesEncrypt($content, $key)
    {
        return openssl_encrypt($content, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, 'ZZWBKJ_ZHIHUAWEI');
    }
  9. thinkphp6的统一返回管理
    /**
     * 返回给前端数据标准化接口
     * @param $data
     * @param $msg
     * @param $code
     * @param $key
     * @return false|string|\think\response\Json
     */
    function apidata($data, $msg, $code, $key)
    {
        $content = [
            "data" => $data,
            "msg" => $msg,
            "code" => $code,
        ];
        if ($key) {
            return base64_encode(aesEncrypt(json_encode($content), $key));
        } else {
            return json($content);
        }
    }

发散思维

  • 我这里是AES密匙这一个应用周期里是重复使用的,服务器重复进行解密获取密匙,其实可以在服务器把AES密匙缓存起来,通过token对比,来获取密匙,这样可以减少服务端的工作量
  • 当然,你也可以一次请求用一次密匙,每次请求都随机生成一个AES密匙,加密发送,这样增加了客户端的工作量
  • AES密匙只存在于本地的内存中,理论上来说,只有用户打开控制台进行变量打印才能知道该密匙,或者黑客控制了该电脑从内存里读出密匙,不要对AES密匙进行本地持久化缓存处理,这样既没必要也增加了AES密匙泄漏的风险

一些坑

  • 主要坑在前后端的加密解密上,js加密php解密,php加密js再解密,因为事先没接触这些,走了很多弯路,以下列出一些对应关系
  • AES加密的参数mode,在php与js端要保持一致,我这里用的CBC
  • AES加密的padding值,js的CryptoJS.pad.ZeroPadding对应php的OPENSSL_ZERO_PADDING,js的CryptoJS.pad.Pkcs7对应php的OPENSSL_RAW_DATA
  • AES加密方法,当密匙长度为32时为‘AES-256-CBC’,16位时则是‘AES-128-CBC’

致谢

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

许可协议

MIT协议

使用中有什么不明白的地方,就向插件作者提问吧~ 我要提问