想把文件递给隔壁 App,却不想把家门钥匙也交出去?”——系统分享 / 文件访问与沙箱边界的实战设计!
跨应用数据交换这件事,99% 的坑都不是“拿不到文件”,而是“给多了权限、锁不住边界、传大文件卡成 PPT”。今天咱就把URI/临时授权大文件分片与零拷贝隐私脱敏这三板斧掰开揉碎,用工程可落地的方式讲清楚,还配上ArkTS 侧代码片段与协议草案。保证你既能把文件递出去,又能把边界守住——**有请“能力化分享、按次授权、带审计的零拷贝”**登场。🙂把“可分享”做成“可控分享”分享这件事,一半是体验
我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~
前言
先来句大实话:跨应用数据交换这件事,99% 的坑都不是“拿不到文件”,而是“给多了权限、锁不住边界、传大文件卡成 PPT”。今天咱就把 URI/临时授权、大文件分片与零拷贝、隐私脱敏这三板斧掰开揉碎,用工程可落地的方式讲清楚,还配上 ArkTS 侧代码片段与协议草案。保证你既能把文件递出去,又能把边界守住——**有请“能力化分享、按次授权、带审计的零拷贝”**登场。🙂
一、目标与威胁模型:先把“坑”画出来
目标:
- App A(分享方)→ App B(接收方)安全交换文件/流/句柄;
- 沙箱原则不破坏:A 不能随意读 B;B 只能按授权读 A 指定对象;
- 大文件不拷贝、不断流、不中断;
- 整条链路可审计、可撤销、可过期;
- 上线合规:最小权限、隐私脱敏、可本地留痕。
威胁:
- 授权过宽(目录递归读、长时可用、可转授);
- URI 被劫持或重放(截屏/粘贴板/日志泄漏);
- 大文件复制造成时间/空间灾难;
- 元数据泄露(EXIF/GPS/人脸/水印);
- 共享后无法撤销或追责。
口号:能力最小化 + 时间窗最短化 + 数据最轻化 + 过程可追溯。
二、总架构:能力导向(Capability-based)而非“路径导向”
┌──────────────┐ ┌──────────────┐
│ App A (源) │ URI │ App B (宿) │
│ Sandboxed ├─────────►│ Sandboxed │
└──────┬───────┘ └──────┬───────┘
│ │
│ grant(token, scope, ttl) │ validate(token)
▼ ▼
ShareProvider / ShareClient /
FileBridge(IPC/Fd) ZeroCopy(MMAP)
│ │
└──────────► OS/Service ⟂ ◄┘
(授权表 / 审计日志 / 回收器)
- URI仅是能力指针(capability),真正的权限来自一次性/短期 token;
- 大文件通过 Fd 直传或 mmap/SharedMemory,避免二次拷贝;
- OS/服务层维护授权表(who→what→until when),支持撤销/过期与审计。
三、URI 与临时授权:令牌就像“门票”,而非“钥匙复印件”
3.1 URI 规范(建议)
caps://share/<resource-id>?t=<opaque_token>&s=<scope>&e=<unix_exp>
resource-id:内容寻址(推荐 CID/哈希)或一次性随机 id;t:不可预测的密文 token(服务端校验或本地验证);s:权限范围(r读 /rw读写 /meta元数据);e:过期时间(**短!**比如 2~10 分钟);
不要在 URI 放明文路径或易识别信息;不要允许二次分享(绑定
aud= 目标包名)。
3.2 授权/校验流程(时序简图)
App A OS/ShareSvc App B
| createCaps() | |
|------------------>| issue token |
|<------------------| (bind aud, ttl) |
| send URI | |
|------------------------------------->|
| validate token |
|<-------------------------------------|
| openFd()/stream | |
|====================> zero-copy read |
- 绑定受众 (
aud):只允许特定包名/签名指纹使用; - 一次性:用后即销,或最少次数(如 1 次读取);
- 最小范围:面向对象授权,不授权目录或模式匹配。
3.3 ArkTS 端(分享方)生成一次性 URI(示例)
// share/Grant.ts
import crypto from '@ohos.security.crypto'
export type Scope = 'r'|'rw'|'meta'
export function createCaps(resourceId: string, audiencePkg: string, scope: Scope, ttlSec = 300) {
const exp = Math.floor(Date.now()/1000) + ttlSec
const nonce = crypto.randomUUID()
const payload = JSON.stringify({ rid: resourceId, aud: audiencePkg, s: scope, e: exp, n: nonce })
const sig = sign(payload) // 私钥/密钥签名(或HMAC)
const token = base64url(enc(payload) + '.' + sig)
const uri = `caps://share/${resourceId}?t=${token}&s=${scope}&e=${exp}`
// 记一笔授权记录(审计/撤销用)
Audit.logGrant({ rid: resourceId, aud: audiencePkg, scope, exp, nonce })
return uri
}
sign()可以落到 本地 Keystore 或私密服务;不要把可验证信息丢到日志。
3.4 接收方校验并打开数据通道(示例)
// share/Client.ts
export async function openCaps(uri: string) {
const u = new URL(uri)
const token = u.searchParams.get('t')!
// 1) 先在本地/服务校验 aud/exp/signature
const ok = await ShareSvc.validate(token, myPackageName(), Date.now())
if (!ok) throw new Error('invalid or expired')
// 2) 申请访问通道:优先 Fd / SharedMemory
return await ShareSvc.openChannel(token) // 返回 { fd? , shmKey? , size , mime }
}
四、大文件分片与零拷贝:让“1GB 文件”不再变“复制地狱”
4.1 传输策略优先级
- 同设备、同 OS:优先 Fd 共享 / mmap(零拷贝);
- 进程隔离:Ashmem/SharedMemory 或 OS 提供的共享区;
- 跨设备 / 云端:分片 + 流式校验 + 断点续传;
- 极端 fallback:短期缓存 + 流读取(谨慎)。
4.2 分片协议(Manifest + Merkle)
- Manifest 记录:chunk size、总 size、每片哈希、顺序;
- Merkle root 作为资源指纹(
resource-id),防篡改、可局部校验; - 支持稀疏拉取与并发下载(B 端按需请求片段)。
Manifest 示例(JSON):
{
"rid": "bafkr...merkle_root",
"size": 104857600,
"chunk": 1048576,
"parts": [
{"i":0,"h":"sha256:..."},
{"i":1,"h":"sha256:..."}
],
"mime": "video/mp4"
}
4.3 ArkTS 端零拷贝(示意)
// share/FileBridge.ts
export async function openChannel(token: string) {
// 通过 token 找到资源路径或文件描述符
const { path, size, mime } = await Index.lookup(token)
// 申请只读 FD 并限制范围(posix_fadvise/allowlist)
const fd = await OS.openReadOnlyFd(path)
return { fd, size, mime }
}
接收方 mmap 读取(伪代码):
// B 进程
const { fd, size, mime } = await ShareSvc.openChannel(token)
// @ts-ignore: native bridge
const view = Native.mmapReadOnly(fd, 0, size)
// 直接把 view 喂给播放器/解析器,无需复制到 JS 堆
优点:常数级内存占用、CPU 不被 memcpy 拖垮;缺点:需要处理页对齐与生命周期(及时
munmap)。
4.4 分片拉流(跨设备/云)ArkTS 侧请求(简化)
// Range 拉片段
async function fetchChunk(rid: string, i: number, chunkSize: number) {
const start = i * chunkSize
const end = start + chunkSize - 1
const resp = await fetch(`/chunks/${rid}`, { headers: { Range: `bytes=${start}-${end}` } })
const data = await resp.arrayBuffer()
const ok = await verifyHash(data, manifest.parts[i].h)
if (!ok) throw new Error('chunk corrupted')
return new Uint8Array(data)
}
五、隐私脱敏:别把“把柄”交出去
5.1 脱敏策略矩阵
| 类型 | 风险 | 处理 |
|---|---|---|
| 图片/视频 EXIF | GPS、设备型号、拍摄时间 | 去 EXIF、可选模糊人脸 |
| 文档(PDF/Office) | 作者、修订历史、评论 | 扁平化导出、清理元数据 |
| 日志/文本 | 手机号、邮箱、姓名 | 正则掩码 + 名单脱敏 |
| 录音/语音 | 私人信息 | 截断静默、变声(选)、加水印提示 |
5.2 ArkTS 侧“分享前处理管线”
// privacy/Sanitize.ts
export interface SanitizeRule { mime: RegExp; run(buf: ArrayBuffer): Promise<ArrayBuffer> }
const stripExif: SanitizeRule = {
mime: /^image\//,
async run(buf) { return await Exif.strip(buf) }
}
const maskText: SanitizeRule = {
mime: /^text\//,
async run(buf) {
const s = new TextDecoder().decode(buf)
const masked = s
.replace(/\b1[3-9]\d{9}\b/g, '1**********')
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, '***@***')
return new TextEncoder().encode(masked).buffer
}
}
export async function sanitizeBeforeShare(file: FileLike) {
const rules = [stripExif, maskText]
let data = await file.readAll()
for (const r of rules) if (r.mime.test(file.mime)) data = await r.run(data)
return { mime: file.mime, data }
}
强制策略:对“外发”场景默认开启脱敏;只有白名单受众(企业内部包签名)才允许跳过某些规则。
5.3 审计与水印(可选)
- 审计记录:
who、what、when、aud、scope、hash; - 视觉水印:对图片/文档添加不可见/半透明标识(含时间戳/设备号);
- 最小留痕:审计日志脱敏,仅存哈希和时间窗。
六、撤销、过期与回收:授权不是“一锤子买卖”
-
时间到即回收:后台定时器扫描授权表,过期即失效;
-
手动撤销:A 方可以在“分享记录”里主动 revoke;
-
活跃句柄处理:对已打开的 Fd,从文件系统层撤销访问较难,策略是:
- 新读请求拒绝,旧句柄仅在 session 内有效;
- 对流式接口,服务端中断(返回 4xx/FIN);
- 对 SharedMemory,设置 版本号/签名 校验,后续消息拒绝。
// revoke.ts
export function revoke(token: string) {
AuthTable.invalidate(token)
Audit.logRevoke(token)
// 如果存在流通道,发送 CLOSE 信号
ChannelHub.closeByToken(token)
}
七、OS/服务端:少就是多(最小可行接口)
7.1 校验接口
POST /caps/validate { token, audience, now } -> { ok, rid, scope, exp }POST /caps/open { token } -> { fd|shmKey|manifest, mime, size }POST /caps/revoke { token } -> { ok }
可以下沉到端内服务(同设备 IPC),或企业网关(跨设备/云)。
7.2 存储设计
- 授权表(KV/SQLite):
token → { rid, aud, scope, exp, nonce, usedCount } - 审计表:
event(time, who, action, rid_hash, audience, scope) - 对象索引:
rid → path/driver/manifest
八、延迟补偿 UI:分享体验“稳、准、快”
- 生成中占位:大文件脱敏/签名时显示“准备中…xx%”,避免误触取消;
- 离线队列:无网时记录分享意图 + 本地 token(仅本机有效),联网后补发;
- 失败可重试:token 过期自动刷新一次(需用户确认);
- 安全提示:目标 App 验证失败时给出明确原因(签名不匹配/过期/撤销)。
// ShareSheet.ets 片段
SButton({ text: 'Share', onPress: async () => {
try {
setState('preparing')
const clean = await sanitizeBeforeShare(file)
const uri = createCaps(clean.rid, targetPkg, 'r', 180)
await sendUriToTarget(uri) // 通过系统分享面板/跨进程
toast('已发送,3分钟内有效')
} catch (e) {
toast(`发送失败:${String(e)}`)
} finally {
setState('idle')
}
}})
九、性能与体积:让“工程处女座”住进你心里
- 零拷贝优先:能 Fd 就别 Buffer,能 mmap 就别读全;
- 分片并发:跨设备拉流并发 =
min(带宽/RTT, CPU/IO 限制),默认 4~8; - Chunk 大小:1–4 MB 较平衡(移动网络);局域网可 8–16 MB;
- 反压与速率:播放器/预览器反馈消费速率,避免缓存暴涨;
- 指数退避:失败重试 3 次,
100ms * 2^n; - 体积预算:脱敏后的临时缓存 ≤ 50MB,超过走管道式处理(边读边脱敏边输出)。
十、常见翻车清单与止血法
-
把目录共享给对方
- 只能授权对象级,禁止递归;URI 每次只对应一个 rid。
-
Token 永不过期
- 统一 TTL(2–10 分钟),完成即销;默认一次性。
-
日志里出现了 URI
- 日志里仅存
rid_hash与事件摘要,URI/token 绝不落日志。
- 日志里仅存
-
EXIF 没清干净
- 统一走分享管线,禁止绕过;CI 做图片/文档元数据扫描。
-
二次分享
- Token 绑定受众包名/签名,App B 再转发 → 校验失败。
-
大文件卡 UI
- 脱敏/签名必须放后台 worker;UI 只显示进度与取消。
-
撤销不生效
- 流式接口必须支持中断信号;本地句柄设置会话版本校验。
十一、可落地代码索引(便于你拷到工程里)
share/Grant.ts:一次性 capability URI 生成 + 审计share/Client.ts:接收方校验与打开通道share/FileBridge.ts:Fd 通道(零拷贝)privacy/Sanitize.ts:分享前脱敏管线revoke.ts:撤销与通道关闭ShareSheet.ets:UI 交互与延迟补偿
结语:把“可分享”做成“可控分享”
分享这件事,一半是体验,一半是边界。当我们用 URI + 临时授权守住入口,用 零拷贝/分片守住性能,用 脱敏 + 审计 + 撤销守住信任,跨应用数据交换就不再是“拆门板”的粗暴动作,而是“交门票”的优雅邀请。下次当产品说“就一个分享按钮嘛”,你可以笑着点头:行,但我们要的是“可控地分享”。😉
…
(未完待续)
更多推荐




所有评论(0)