本次目标是制作一个密码记录工具,为保证私密性,数据保存在本地并通过直传方式备份到阿里云 OSS(需要用户开启自动备份并填写 AccessKeyID、AccessKeySecret)
代码详见:个人密码本
AES
加密,此后需要用户输入密码才能查看)工具运行流程图如下:
开始动手前,先了解下小程序中的数据存储
小程序文件系统
类型 | 读权限 | 写权限 | 容量限制 |
---|---|---|---|
Storage | ✅ | ✅ | 单个 KEY 上限 1M,总上限 10M |
临时文件 | ✅ | 无限制 | |
缓存文件 | ✅ | 与用户文件共计不超过 200M | |
用户文件 | ✅ | ✅ | 与缓存文件共计不超过 200M |
综上,开发者能够写数据的只有Storage
跟本地用户文件
,前者适合数据量小的场景。开发者可通过wx.getStorage
、wx.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()
}
}
对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。
阿里云有官方SDK ali-oss,但在小程序中无法正常使用,故这里使用的是 WEB 直传方式(也是官方推荐)
阿里云 OSS 官方文档: URL自签 、小程序直传
注意
weapp-tools
Storage
(type=0)类型数据时,先保存到用户文件,完成上传后删除该文件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
上查看完整代码。由于时间仓促代码可能有问题,如发现请联系我纠正。
往期文章