小程序实战三:本地数据存储及阿里云OSS备份(个人密码本示例)

功能描述

本次目标是制作一个密码记录工具,为保证私密性,数据保存在本地并通过直传方式备份到阿里云 OSS(需要用户开启自动备份并填写 AccessKeyID、AccessKeySecret)
代码详见:个人密码本

小程序实战三:本地数据存储及阿里云OSS备份(个人密码本示例)_第1张图片

  1. 用户编辑密码信息,包含类型(生活类、工作类、其他)、网站名称、登录名、密码(便于记忆的提示性文字)
  2. 小程序将数据保存到 Storage(默认明文,若设置了查看密码,则通过AES加密,此后需要用户输入密码才能查看)
  3. 若启用自动备份则将数据上传到阿里云 OSS

工具运行流程图如下:

小程序实战三:本地数据存储及阿里云OSS备份(个人密码本示例)_第2张图片

相关知识

开始动手前,先了解下小程序中的数据存储

小程序文件系统

类型 读权限 写权限 容量限制
Storage 单个 KEY 上限 1M,总上限 10M
临时文件 无限制
缓存文件 与用户文件共计不超过 200M
用户文件 与缓存文件共计不超过 200M

综上,开发者能够写数据的只有Storage本地用户文件,前者适合数据量小的场景。开发者可通过wx.getStoragewx.setStorage方法读写 Storage;通过 FileSystemManager 来管理用户文件。

此处我以 type = 0 表示Storage、type=1 表示用户文件,并对持久化数据读写进行了简单的封装(写方法执行成功后会触发自动备份子流程):

module.exports = {
    /**
     * 将数据保存到 storage,保存成功后触发自动备份
     * @param {*} key       若为空则自动根据当前页计算,命名方式为:{当前页名称}.{key}
     * @param {*} data      待保存的数据
     * @param {*} onOk      
     * @param {*} onFail 
     */
    toStorage (key, data, onOk, onFail=defFailAct){
        key = !!key? key: util.buildUrlKey(key)
        console.debug(`保存到 Storage, key=`, key)
        wx.setStorage({ 
            data, 
            key, 
            success: res=>{
                !onOk || onOk(res)
                //触发自动备份
                backup.onDataChange(key, 0)
            }, 
            fail: onFail 
        })
    },
    /**
     * 从 storage 加载数据
     * @param {*} key 
     * @param {*} onOk  注意:若storage不存在,不执行该方法
     */
    fromStorage (key, onOk){
        key = !!key? key: util.buildUrlKey(key)
        wx.getStorage({key, success:res=> !onOk || onOk(res.data)})
    },
    /**
     * 将数据写入到文件(将转换为 JSON 格式),保存成功后触发自动备份
     * @param {*} name 
     * @param {*} data 
     * @param {*} onOk 
     * @param {*} onFail 
     */
    toFile (name, data, onOk, onFail=defFailAct){
        let filePath = !!name? util.buildPath(name): buildFilePath(name)
        console.debug(`保存到文件,name=`, filePath)
        wx.getFileSystemManager().writeFile({
            filePath,
            data: JSON.stringify(data),
            success: d=>{
                !onOk || onOk(d)
                //触发自动备份
                backup.onDataChange(key, 1)
            },
            fail: onFail
        })
    },
    /**
     * 从文件中读取内容
     * @param {*} name      默认会以当前url
     * @param {*} onOk 
     * @param {*} onFail 
     */
    fromFile (name, onOk, onFail=defFailAct){
        let filePath = !!name? util.buildPath(name): buildFilePath(name)
        wx.getFileSystemManager().readFile({
            filePath,
            encoding:"utf8",
            success: res=>{
                !onOk || onOk(JSON.parse(res.data))
            },
            fail: onFail
        })
    }
}

相关示例:本地存储

功能实现

账号信息读取

_loadData (){
    store.fromStorage("", data=>{
        let items = undefined
        //如果以 [ 开头就是明文
        if(util.isJSONArrayText(data)){
            items = JSON.parse(data)
        }else {
            let needToInputPwd = true
            if(aesKey){
                //尝试解密
                try{
                    let rawText = aes.decrypt(aesKey, data)
                    if(util.isJSONArrayText(rawText)){
                        items = JSON.parse(rawText)
                        needToInputPwd = false
                    }
                }catch(decryptE){
                    console.error(`解密数据出错:`, decryptE.message)
                }
            }

            if(needToInputPwd){
                if(!this.data.lockShow){
                    //此处设置延迟执行,否则将出现 dialog 错误
                    setTimeout(this.toLock, 200)
                }
                this.setData({needReload: true})
                return util.warn(`请输入查看密码`)
            }
        }

        if(Array.isArray(items)){
            if(this.data.keyword){
                items = items.filter(v=> `${v.site}${v.name}`.indexOf(this.data.keyword)>-1)
            }
            fixColor(items)
            this.setData({ items })
        }
    })
}

数据加解密

代码详见:utils/secret.js

const CryptoJS = require("./plugins/CryptoJS")
const md5 = require("./plugins/md5.min")

const SALT = "WeappTools2020"
/**
 * AES 功能
 * 密钥为任意长度字符串(支持中文)
 * 密钥生成规则: md5("原始密钥+盐")
 */
let aes = {
    iv: CryptoJS.enc.Utf8.parse('0604hxWeappTools'),
    buildKey (originText){
        let key = md5(`${originText}${SALT}`, SALT)
        return CryptoJS.enc.Utf8.parse(key)
    },
    encrypt (key, text){
        let encrypted = CryptoJS.AES.encrypt(
            CryptoJS.enc.Utf8.parse(text), 
            this.buildKey(key), 
            {
                iv: this.iv, 
                mode: CryptoJS.mode.CBC,
                padding: CryptoJS.pad.Pkcs7
            }
        )
        return encrypted.ciphertext.toString()
    },
    decrypt (key, encryptedText){
        let encryptedHexStr = CryptoJS.enc.Hex.parse(encryptedText)

        let decrypt = CryptoJS.AES.decrypt(
            CryptoJS.enc.Base64.stringify(encryptedHexStr), 
            this.buildKey(key), 
            {
                iv: this.iv,
                mode: CryptoJS.mode.CBC,
                padding: CryptoJS.pad.Pkcs7
            }
        )
        return decrypt.toString(CryptoJS.enc.Utf8).toString()
    }
}

备份到 OSS

对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。
阿里云有官方SDK ali-oss,但在小程序中无法正常使用,故这里使用的是 WEB 直传方式(也是官方推荐)

阿里云 OSS 官方文档: URL自签 、小程序直传

注意

  • 由于阿里云 OSS 每个 bucket 域名都不一致,小程序中需要配置域名白名单(不支持通配符)才能使用 request 访问,所以 bucket 此处必须为weapp-tools
  • 备份 Storage (type=0)类型数据时,先保存到用户文件,完成上传后删除该文件
  • 为区分 DEV 及线上环境,开发环境下备份文件将加上dev标识
let SETTING_PAGE = "pages/system/setting/setting"

/**
 * @param {*} names     需要加载的配置项
 * @param {*} onOk      回调函数
 * @param {*} modal     配置项缺失时是否弹出前往配置页面的对话框
 * @param {*} prefix    配置项前缀,默认为空
 */
let withSetting = (names, onOk, modal = false, prefix = "") => {
    let {
        config
    } = getApp().globalData
    let fields = names.map(n => config[`${prefix}${n}`])
    //检查为空
    let fieldSize = fields.filter(f => !!f).length
    if (fieldSize == fields.length) {
        onOk(...fields)
    } else if (modal) {
        wx.showModal({
            title: '未配置OSS',
            content: '请先到设置页面填写对象存储服务(Object Storage Service,OSS)相关参数才可使用该功能',
            confirmText: "前往设置",
            cancelText: "下次再说",
            confirmColor: "red",
            success: res => {
                if (res.confirm) {
                    let pages = getCurrentPages()
                    if (pages[pages.length - 1].route != SETTING_PAGE)
                        this.jumpTo(SETTING_PAGE)
                }
            }
        })
    } else
        console.debug(`[OSS] 配置项缺失,需要 ${names} 只找到 ${fieldSize} 个有效值...`)
}

// 详细代码在:utils\oss\oss.js
/**
 * 阿里云 OSS 模块
 */
let Aliyun = {
    keys: ["OSS_ALI_ID", 'OSS_ALI_SECRET', 'OSS_ALI_REGION'],

    config: {
        host: `https://weapp-tools.oss-cn-#region#.aliyuncs.com`,
        agent: "aliyun-sdk-js/6.9.0 Chrome 87.0.4280.67 on OS X 10.14.2 64-bit",
        timeout: 87600,                     // 文件失效时间
        region: "oss-cn-#region#",
        bucket: "weapp-tools",
        maxSize: 10 * 1024 * 1024       // 设置上传文件的大小限制(此处使用10M)
    },
    /**
     * 构建相关策略
     */
    getPolicy() {
        let date = new Date();
        date.setHours(date.getHours() + this.config.timeout);
        let expire = date.toISOString();
        const policy = {
            "expiration": expire, // 设置该Policy的失效时间
            "conditions": [
                ["content-length-range", 0,  this.config.maxSize] 
            ]
        }

        return Base64.encode(JSON.stringify(policy));
    },
    // 用密钥对数据进行加密
    getSignature(policyBase64, secretKey) {
        const bytes = Crypto.HMAC(
            Crypto.SHA1, policyBase64,
            secretKey, {
                asBytes: true
            }
        )
        return Crypto.util.bytesToBase64(bytes)
    },

    upload(filePath, targetPath, silent=true) {
        return new Promise((resolve, reject) => {
            withSetting(
                this.keys, 
                (accessKeyId, accessKeySecret, region) => {
                    let policy = this.getPolicy()
                    console.debug(`[OSS Aliyun] 保存到 ${targetPath} policy=${policy}`)
                    wx.uploadFile({
                        url:    this.config.host.replace("#region#", region),
                        filePath,
                        name: 'file',
                        formData: {
                            'key':  targetPath, // 服务利用key找到文件
                            'policy': policy,
                            'OSSAccessKeyId': accessKeyId,
                            'signature': this.getSignature(policy, accessKeySecret),
                            'success_action_status': '200',
                        },
                        success: function (res) {
                            if (res.statusCode != 200) {
                                res.message = /(.+)<\/Message>/.exec(res.data)[1]
                                reject(res)
                                return
                            }
                            resolve({name: targetPath, server:"aliyun"}, res)
                        },
                        fail: err=> {
                            err.wxaddinfo = this.config.host
                            err.message = "微信接口 uploadFile 调用失败"
                            reject(err)
                        }
                    })
                }, 
                !silent
            )
        })
    }
}

写在最后

这次贴了好几段长代码(偷懒),而且是完整模块截取出来的,看起来会比较难受,有兴趣的朋友可以到github上查看完整代码。由于时间仓促代码可能有问题,如发现请联系我纠正。


往期文章

  1. 微信小程序开发基础(胎儿体重测算工具实例)
  2. 云函数开发及本地调试(意见反馈实例)

扫码或微信搜索集成工具集体验小程序 关注公众号
扫码(微信搜索集成工具集)体验小程序 / 关注公众号

你可能感兴趣的:(前端,微信小程序,阿里云OSS,个人密码管理)