跨应用数据共享提供了向其他应用共享以及管理其数据的方法,支持不同应用之间的数据协同。这篇文章我们继续探讨一下鸿蒙应用组件 DataShareExtensionAbility
的开发和安全注意事项。DataShareExtensionAbility
顾名思义数据共享组件,当跨应用访问数据时,可以通过DataShareExtensionAbility
拉起数据提供方的应用以实现对数据的访问。此种方式支持跨应用拉起数据提供方的 DataShareExtensionAbility
,数据提供方的开发者可以在回调中实现灵活的业务逻辑,用于跨应用复杂业务场景。数据共享可分为数据的提供方和访问方两部分:
• 数据提供方:DataShareExtensionAbility,可以选择性实现数据的增、删、改、查,以及文件打开等功能,并对外共享这些数据。
• 数据访问方:由 createDataShareHelper 方法所创建的工具类,利用工具类,便可以访问提供方提供的这些数据。
图摘自test.openharmony.cn:7780/pages/v4.0/zh-cn/application-dev/database/share-data-by-datashareextensionability.md
• DataShareExtensionAbility 模块为数据提供方,实现跨应用数据共享的相关业务。
• DataShareHelper 模块为数据访问方,提供各种访问数据的接口,包括增删改查等。
• 数据访问方与提供方通过IPC进行通信,数据提供方可以通过数据库实现,也可以通过其他数据存储方式实现。
• ResultSet 模块通过共享内存实现,用于存储查询数据得到的结果集,并提供了遍历结果集的方法。
在项目工程中的module.json5
添加DataShareExtensionAbility
组件的配置信息
"extensionAbilities": [
{
"name": "DataShareExtAbility",
"srcEntrance": "./ets/DataShareExtensionAbility/DataShareExtAbility.ts",
"icon": "$media:icon",
"description": "$string:DataShareExtensionAbility_desc",
"type": "dataShare",
"uri": "datashare://com.example.mydatashareextensionability.DataShare",
"exported": true
}
]
相关配置说明如下
属性名称 | 备注说明 | 必填 |
name | Ability名称,对应Ability派生的ExtensionAbility类名。 | 是 |
type | Ability类型,DataShare对应的Ability类型为“dataShare”,表示基于datashare模板开发的。 | 是 |
uri | 通信使用的URI,是客户端链接服务端的唯一标识。 | 是 |
exported | 对其他应用是否可见,设置为true时,才能与其他应用进行通信传输数据。 | 是 |
readPermission | 访问数据时需要的权限,不配置默认不进行读权限校验。 | 否 |
writePermission | 修改数据时需要的权限,不配置默认不进行写权限校验。 | 否 |
metadata | 增加静默访问所需的额外配置项,包含name和resource字段。 | |
name类型固定为"ohos.extension.dataShare",是配置的唯一标识。 | ||
resource类型固定为"$profile:data_share_config",表示配置文件的名称为data_share_config.json。 | 否 |
data_share_config.json
对应属性字段
实现一个简单的 DataShareExtensionAbility
import DataShareExtensionAbility from '@ohos.application.AccessibilityExtensionAbility'
import relationalStore from '@ohos.data.relationalStore'const TAG: string = 'DataShareExtAbility'
const TABLE_NAME: string = 'contacts'
const STORE_CONFIG: relationalStore.StoreConfig = { name: 'contacts.db', securityLevel: relationalStore.SecurityLevel.S1 }
const SQL_CREATE_TABLE: string = 'CREATE TABLE IF NOT EXISTS person (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT NOT NULL)'
let rdbStore: relationalStore.RdbStore = undefined
export default class DataShareExtAbility extends DataShareExtensionAbility {
onCreate(want, callback) {
relationalStore.getRdbStore(this.context, STORE_CONFIG, (err, data) => {
if (err) {
Logger.error(TAG, `DataShareExtAbility getRdbStore err : ${JSON.stringify(err)}`)
} else {
rdbStore = data
if (rdbStore != undefined) {
rdbStore.executeSql(SQL_CREATE_TABLE, [], () => {
Logger.info(TAG, `DataShareExtAbility executeSql done`)
})
}
if (callback) {
callback()
}
}
})
}
insert(uri, value, callback) {
Logger.info(TAG, `[insert] enter`)
if (value === null) {
Logger.info(' [insert] invalid valueBuckets')
return
}
if (rdbStore) {
rdbStore.insert(TABLE_NAME, value, (err, ret) => {
if (callback !== undefined) {
callback(err, ret)
}
})
}
}
delete(uri, predicates, callback) {
Logger.info(TAG, `delete`)
try {
if (rdbStore) {
rdbStore.delete(TABLE_NAME, predicates, (error, ret) => {
Logger.info(TAG, `delete ret: ${ret}`)
callback(error, ret)
})
}
} catch (error) {
Logger.error(TAG, `delete error: ${JSON.stringify(error)}`)
}
}
query(uri, predicates, columns, callback) {
Logger.info(TAG, `query enter`)
try {
if (rdbStore) {
rdbStore.query(TABLE_NAME, predicates, columns, (err, resultSet) => {
if (resultSet !== undefined) {
Logger.info(TAG, `query resultSet.rowCount: ${JSON.stringify(resultSet.rowCount)}`)
}
if (callback !== undefined) {
callback(err, resultSet)
}
})
}
} catch (err) {
Logger.error(TAG, `query error: ${JSON.stringify(err)}`)
}
}
update(uri, predicates, value, callback) {
if (predicates === null || predicates === undefined) {
return
}
if (rdbStore) {
rdbStore.update(TABLE_NAME, value, predicates, function (err, ret) {
if (callback !== undefined) {
callback(err, ret)
}
})
}
}
}
如上,DataShareExtensionAbility提供以下API,根据需要重写对应回调方法
• onCreate:DataShare客户端连接DataShareExtensionAbility服务端时,服务端需要在此回调中实现初始化业务逻辑,该方法可以选择性重写。
• insert:业务函数,客户端请求插入数据时回调此接口,服务端需要在此回调中实现插入数据功能,该方法可以选择性重写。
• update:业务函数,客户端请求更新数据时回调此接口,服务端需要在此回调中实现更新数据功能,该方法可以选择性重写。
• delete:业务函数,客户端请求删除数据时回调此接口,服务端需要在此回调中实现删除数据功能,该方法可以选择性重写。
• query:业务函数,客户端请求查询数据时回调此接口,服务端需要在此回调中实现查询数据功能,该方法可以选择性重写。
• batchInsert:业务函数,客户端请求批量插入数据时回调此接口,服务端需要在此回调中实现批量插入数据的功能,该方法可以选择性重写。
• normalizeUri:业务函数,客户端给定的URI转换为服务端使用的URI时回调此接口,该方法可以选择性重写。
• denormalizeUri:业务函数,服务端使用的URI转换为客户端传入的初始URI时服务端回调此接口,该方法可以选择性重写。
DataShareHelper
管理工具实例,可使用此实例访问或管理服务端的数据。在调用DataShareHelper
提供的方法前,需要先通过 createDataShareHelper
构建一个实例。定义与数据提供方通信的 URI 字符串
// 作为参数传递的URI,与module.json5中定义的URI的区别是多了一个"/",是因为作为参数传递的URI中,在第二个与第三个"/"中间,存在一个DeviceID的参数
let BASE_URI = ('datashare:///com.example.mydatashareextensionability.DataShare');
创建工具接口类对象
let dataShareHelper = await dataShare.createDataShareHelper(context, BASE_URI)
获取到接口类对象后,便可利用其提供的接口访问提供方提供的服务,如进行数据的增删改查等
// 增加数据
let valuesBuckets = { name: 'lilei', phone: '123456' }
dataShareHelper.insert(BASE_URI, valuesBuckets)// 删除数据
let predicates = new dataSharePredicates.DataSharePredicates()
predicates.equalTo('id', person.id)
let num = await this.dataShareHelper.delete(BASE_URI, predicates)
// 查找数据
const COLUMNS = ['*']
let predicates = new dataSharePredicates.DataSharePredicates()
let resultSet = await this.dataShareHelper.query(BASE_URI, predicates, COLUMNS)
// 更新数据
let valuesBucket = { name: 'lilei', phone: '654321' }
let predicates = new dataSharePredicates.DataSharePredicates()
predicates.equalTo('name', 'lilei')
dataShareHelper.update(BASE_URI, predicates, valuesBucket)
订阅者可以向指定的uri
注册观察者,当有其他用户触发变更通知时,订阅者将收到callback
通知并调用callback
异步回调
// 用户B 触发变更通知
dataShareHelper.notifyChange(BASE_URI)// 用户A(订阅者)注册观察者
let onCallback: () => void = (): void => {
console.info("**** Observer on callback ****");
}
if (dataShareHelper !== undefined) {
(dataShareHelper as dataShare.DataShareHelper).on("dataChange", BASE_URI, onCallback);
}
//用户A(订阅者)取消观察者
let callback: () => void = (): void => {
console.info("**** Observer on callback ****");
}
if (dataShareHelper != undefined) {
(dataShareHelper as dataShare.DataShareHelper).off("dataChange", BASE_URI, callback);
}
最后我们测试一下订阅者从 DataShareExtensionAbility
中获取数据,如下图
默认情况下,应用只能访问有限的系统资源。但某些情况下,应用为了扩展功能的诉求,需要访问额外的系统或其他应用的数据(包括用户个人数据)、功能。系统或应用也必须以明确的方式对外提供接口来共享其数据或功能。DataShareExtensionAbility
作为数据提供者,必须要考虑的就是数据访问权限的安全问题。OpenHarmony
提供了一种访问控制机制来保证这些数据或功能不会被不当或恶意使用,即应用权限。在项目文件 module.json5
定义的 DataShareExtensionAbility
中可以通过配置如下字段来添加对应的读写权限
属性名称 | 备注说明 | 必填 |
readPermission | 访问数据时需要的权限,不配置默认不进行读权限校验。 | 否 |
writePermission | 修改数据时需要的权限,不配置默认不进行写权限校验。 | 否 |
同样我们想如Android一样通过声明自定义权限来对接口做访问权限控制。在官方文档中(./docs/zh-cn/application-dev/quick-start/module-structure.md)有如下说明:definePermission
仅支持系统应用配置,三方应用配置不生效。本身我们实现的DataShareExtensionAbility
应用即为系统应用(非系统应用无法使用)。definePermissions
对象内部结构如下
那么我们在数据提供者的 module.json5
中添加如下配置项
"extensionAbilities": [
{
///....
"readPermission": "com.MyDataShareExtensionAbility.PERMISSION.READ",
"writePermission":"com.MyDataShareExtensionAbility.PERMISSION.WRITE",
"permissions": [
"com.MyDataShareExtensionAbility.PERMISSION.READ",
"com.MyDataShareExtensionAbility.PERMISSION.WRITE"
],
}
],
"definePermissions": [
{
"name": "com.MyDataShareExtensionAbility.PERMISSION.READ",
"grantMode": "system_grant"
},
{
"name": "com.MyDataShareExtensionAbility.PERMISSION.WRITE",
"grantMode": "system_grant"
}
]
此时再通过访问方应用将无法获取 mydatashareextensionability
的数据,Log 权限校验信息如下,提示"verify access token fail, permission: com.MyDataShareExtensionAbility.PERMISSION.READ"
可以看到添加的权限校验生效了。那么我们在客户端添加 com.MyDataShareExtensionAbility.PERMISSION.READ
是否可以成功访问目标呢?答案是不行的,通过分析Openharmony的源码可以知道当前版本不支持除ohos.global.systemres
外的应用自定义权限,即客户端通过配置或者动态申请权限会失败
Openharmony 安装的应用信息注册&持久化以及权限校验的代码流程图如下所示
目前Openharmony内置的权限列表见 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/permission-list-0000001544464017-V2
至此关于暴露的接口权限控制我们做一个小结,除 ohos.global.systemres
外不支持其他应用自定义权限。当未对 DataShareextensionAbility
配置访问权限时,我们作为开发者必须要考虑到的问题是,业务使用方可以随意访问我们实现的增删查改的功能,这其中是否存在安全风险 ,以及我们提供的数据中是否包含有敏感数据,都是需要开发者合理审视。若对 DataShareextensionAbility
配置了访问权限:
1)readPermission / writePermission
可以不配置也可以配置其中之一或者全部配置,若配置了 readPermission / writePermission
中任一权限,则数据访问方连接数据提供方时必须满足已配置的 readPermission
或者 writePermission
;
2)若未配置 readPermission / writePermission
,但配置了 permissions
,则数据访问方连接数据提供方时需要满足 permissions
中的任一权限;
3)当仅配置了 readPermission
,则 insert/delete/update/batchInsert
接口对外暴露;
4)当仅配置了 writePermission
,则 query
接口对外暴露。
DataShareextensionAbility
基于关系型数据库,提供两种查询数据的方式:
• 直接调用查询接口。使用该接口,会将包含查询条件的谓词自动拼接成完整的SQL语句进行查询操作,无需用户传入原生的SQL语句。
• 执行原生的SQL语句进行查询操作。
当使用原生的SQL语句进行操作时,如 querySql/executeSql
等直接传入sql语句作为参数,注意其sql语句是否包含来自外部数据拼接。如下我们选取 querySql
测试下,修改上文中的 DEMO 的查询语句
let sqli = '1 union select 1,\'sqlinject\',sqlite_version()';
let sqlquery = 'select * from person where id=' + sqli
rdbStore.querySql(sqlquery, [], (err, resultSet) => {
Logger.info(TAG, `queryvuln ret: ${resultSet}`)
if (resultSet !== undefined) {
Logger.info(TAG, `queryvuln resultSet.rowCount: ${JSON.stringify(resultSet.rowCount)}`)
}
if (callback !== undefined) {
callback(err, resultSet)
}
})
可以看到回显出 sqlite 的版本信息
建议使用数据库条件的谓词 Predicates ,可以设置查询条件。如上文查询条件id=1
,可以改写成如下代码
let predicates = new dataSharePredicates.DataSharePredicates()
predicates.equalTo('ID', '1');
let resultSet = await this.dataShareHelper.query(BASE_URI, predicates, COLUMNS)
return resultSet
除了equalTo
外,Predicates
还提供了如 notEqualTo
beginWrap
like
等等,具体请参看 Openharmony
源码中的 Predicates
相关类提供的接口。
如果应用中使用了 RdbStore
类的 querySql
、querySqlWithHook
、querySqlByStep
、executeSql
等直接传入原生SQL
语句作为参数的函数,那么需要注意检查第一个参数,即SQL
语句的字符串中是否包含来自外部输入的拼接, 如果存在的话则需要开发人员编写完善的安全策略以防止SQL
注入漏洞的产生。所以我们还是建议开发者使用 Openharmony
封装好的 Predicates
相关接口来配置数据访问。
1. http://test.openharmony.cn:7780/pages/v4.0/zh-cn/application-dev/reference/apis/js-apis-data-dataShare.md
2. https://gitee.com/openharmony/app_samples/tree/master/ability/ServiceExtAbility
3. https://developer.huawei.com/consumer/cn/forum/topic/0203143504490236581