HarmonyOS Pasteboard 安全复制:MIME 分类、敏感内容与跨应用边界

请添加图片描述

复制功能看起来只有一次 setData(),实际却把内容交给了系统级共享通道。若应用把登录令牌、完整订单对象或内部文件路径当普通文本写入,其他应用的粘贴场景可能接触到这些内容;若所有类型都用 text/plain,接收方又无法判断应该显示文字、打开 URI,还是解析结构化数据。功能能用,并不等于边界正确。

本文以“笔记应用复制标题、富文本和分享地址”为例,围绕 PasteData、MIME 类型、ShareOption、读取前校验和主动清理,建立一条可审计的剪贴板读写链路。重点不是封装一个工具函数,而是让每一种复制行为都有明确的数据最小集、作用范围和失效时机。

1. 先把复制操作当成一次数据发布

写入系统剪贴板后,原页面不再独占这份数据。设计复制功能时应先回答:用户明确选择了什么、接收方需要哪种格式、是否允许跨应用、内容应该保留多久。

export interface CopyIntent {
  scene: 'note_title' | 'note_rich_text' | 'share_link';
  displayText: string;
  htmlText?: string;
  uri?: string;
  scope: 'in_app' | 'local_device';
  sensitive: boolean;
}

export type PasteResult =
  | { kind: 'text'; value: string }
  | { kind: 'uri'; value: string }
  | { kind: 'unsupported' };

CopyIntent 不接受任意业务对象,从入口就阻止调用方把整份用户模型传给剪贴板层。sensitive 不是装饰字段,它决定是否允许写入、是否仅应用内使用以及是否安排清理。

2. API 基线与 MIME 语义

本文以 Stage 模型、ArkTS、HarmonyOS SDK API 23 为基线,使用 @kit.BasicServicesKit 中的 pasteboardcreateData()SystemPasteboard.setData()getData()hasData()clearData() 的 Promise 形式从 API 9 起可用;创建同一记录的多种 MIME 表示从 API 14 起可用。

MIME 类型 常量 值的用途 不应装入什么
纯文本 MIMETYPE_TEXT_PLAIN 标题、摘要、普通文字 JSON 化的完整账号对象
HTML MIMETYPE_TEXT_HTML 富文本片段 未清理的脚本或私有属性
URI MIMETYPE_TEXT_URI 资源引用、网页地址 无授权的沙箱路径
Want MIMETYPE_TEXT_WANT 明确的能力意图 用户不可见的后台拉起参数
PixelMap MIMETYPE_PIXELMAP 图像像素数据 可用 URI 表达的大文件副本

接收方应根据 MIME 解析,不能先调用 getPrimaryText(),失败后再猜是不是 URI。自定义 MIME 字符串也有长度约束,跨应用前还需约定格式版本。

3. 纯文本写入只保留用户看得见的字段

最常见的错误是为了“粘贴后还能恢复上下文”,把内部 ID、调试信息和鉴权参数一起拼进文本。复制标题时只写标题;业务关联信息留在应用内部。

import { pasteboard } from '@kit.BasicServicesKit';

export async function copyNoteTitle(title: string): Promise<void> {
  const normalized = title.trim().slice(0, 500);
  if (normalized.length === 0) throw new Error('empty note title');

  const data = pasteboard.createData(
    pasteboard.MIMETYPE_TEXT_PLAIN,
    normalized
  );
  const property = data.getProperty();
  property.shareOption = pasteboard.ShareOption.LOCALDEVICE;
  data.setProperty(property);

  await pasteboard.getSystemPasteboard().setData(data);
}

长度限制是项目策略,用于避免一次复制带出超长页面内容。LOCALDEVICE 允许本设备其他应用接收;只供本应用组件粘贴时应改为 INAPP

4. 从构造到清理形成完整读写闭环

请添加图片描述

读写顺序应固定为:按场景构造最小数据、标注 MIME 与分享范围、在用户明确操作后写入、读取时先校验类型、敏感场景完成后主动清理。剪贴板不是应用数据库,不能承担长期保存职责。

import { pasteboard } from '@kit.BasicServicesKit';

export async function clearCopiedSecret(): Promise<void> {
  const board = pasteboard.getSystemPasteboard();
  if (await board.hasData()) {
    await board.clearData();
  }
}

主动清理要与业务状态绑定,例如一次性恢复码被粘贴成功或页面超时后清理。不要在任意页面进入时清空系统剪贴板,这会破坏用户从其他应用复制的内容。

5. ShareOption 决定数据能传播到哪里

PasteDataProperty.shareOption 可设置 INAPPLOCALDEVICE。不显式设置范围会让行为依赖默认策略,安全边界难以审查。跨设备相关能力还受系统版本和设备策略影响,不能仅通过旧枚举推断可用性。

function limitScope(
  data: pasteboard.PasteData,
  scope: CopyIntent['scope']
): pasteboard.PasteData {
  const property = data.getProperty();
  property.shareOption = scope === 'in_app'
    ? pasteboard.ShareOption.INAPP
    : pasteboard.ShareOption.LOCALDEVICE;
  data.setProperty(property);
  return data;
}

登录临时码、草稿恢复片段等即使允许复制,也更适合 INAPP。分享链接本来就是给其他应用使用的,才选择 LOCALDEVICE

6. 读取前先确认是否有数据和目标类型

粘贴按钮点击后,先判断系统剪贴板是否有内容,再取得 PasteData,最后用 hasType() 验证。这样可以把空数据、类型不支持和读取异常分成不同页面状态。

import { pasteboard } from '@kit.BasicServicesKit';

export async function readPreferredContent(): Promise<PasteResult> {
  const board = pasteboard.getSystemPasteboard();
  if (!await board.hasData()) return { kind: 'unsupported' };

  const data = await board.getData();
  if (data.hasType(pasteboard.MIMETYPE_TEXT_URI)) {
    return { kind: 'uri', value: data.getPrimaryUri() };
  }
  if (data.hasType(pasteboard.MIMETYPE_TEXT_PLAIN)) {
    return { kind: 'text', value: data.getPrimaryText() };
  }
  return { kind: 'unsupported' };
}

读取顺序体现业务优先级。分享场景优先 URI,编辑器场景可能优先 HTML。不要把未知二进制记录强制转换成字符串。

7. 一份内容可以提供多种表示

API 14 起,createData(Record<string, ValueType>) 可以为同一份剪贴板数据提供多种 MIME 表示。富文本编辑器可以同时提供 HTML 和纯文本,使不支持 HTML 的接收方仍能降级粘贴。

import { pasteboard } from '@kit.BasicServicesKit';

export async function copyRichNote(plain: string, html: string): Promise<void> {
  const data = pasteboard.createData({
    [pasteboard.MIMETYPE_TEXT_PLAIN]: plain,
    [pasteboard.MIMETYPE_TEXT_HTML]: html
  });
  limitScope(data, 'local_device');
  await pasteboard.getSystemPasteboard().setData(data);
}

两种表示必须表达同一内容,不能在 HTML 中夹带纯文本版本没有的隐藏业务数据。HTML 还应在进入剪贴板前经过项目自己的安全净化流程。

8. URI 是引用,不等于接收方天然有权限

把文件路径写成 text/uri 只传递了定位信息,不会自动授予对应用沙箱文件的读取权。跨应用共享文件时应使用平台支持的文件分享机制和正确 URI,并确保接收方拥有必要访问能力。

function validateShareUri(uri: string): string {
  const value = uri.trim();
  const allowed = value.startsWith('https://') || value.startsWith('file://');
  if (!allowed) throw new Error('unsupported share uri');
  if (value.includes('token=') || value.includes('access_key=')) {
    throw new Error('credential must not be copied');
  }
  return value;
}

示例只做最外层防线,生产项目还应解析 URI 结构,避免靠字符串包含判断安全性。带签名的临时下载地址也可能泄露访问能力,不应直接进入系统剪贴板。

9. 敏感内容需要显式用户动作和短生命周期

密码、令牌和身份凭据不应提供复制功能。业务确实需要一次性恢复码时,必须由用户点击触发,缩短内容、限制作用范围,并在完成后清理。

export class SensitiveCopySession {
  private active: boolean = false;

  async copyOnce(code: string): Promise<void> {
    if (!/^\d{6}$/.test(code)) throw new Error('invalid one-time code');
    const data = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, code);
    limitScope(data, 'in_app');
    await pasteboard.getSystemPasteboard().setData(data);
    this.active = true;
  }

  async finish(): Promise<void> {
    if (!this.active) return;
    await clearCopiedSecret();
    this.active = false;
  }
}

清理时还要防止误删用户后来复制的新内容。更严谨的实现应保存本次复制的短期指纹,清理前重新读取并确认仍是本会话写入的数据。

10. 责任边界阻止页面直接操作系统剪贴板

请添加图片描述

业务层选择最小字段,编码层构造 PasteData,系统剪贴板承载共享状态,接收应用按用户粘贴动作读取。页面不应把任意对象直接传给 setData(),也不应在后台轮询其他应用复制了什么。

export interface ClipboardGateway {
  write(intent: CopyIntent): Promise<void>;
  readForEditor(): Promise<PasteResult>;
  clearOwnedSensitiveData(): Promise<void>;
}

export class SystemClipboardGateway implements ClipboardGateway {
  async write(intent: CopyIntent): Promise<void> {
    if (intent.sensitive && intent.scope !== 'in_app') {
      throw new Error('sensitive content cannot leave app');
    }
    const data = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, intent.displayText);
    limitScope(data, intent.scope);
    await pasteboard.getSystemPasteboard().setData(data);
  }

  readForEditor(): Promise<PasteResult> { return readPreferredContent(); }
  clearOwnedSensitiveData(): Promise<void> { return clearCopiedSecret(); }
}

网关集中记录场景、范围和失败原因,也便于在不同设备形态下替换权限与交互策略。

11. 并发复制需要串行化而不是无限重试

系统可能返回“另一个复制或粘贴操作正在进行”的错误。连续点击复制按钮时,应用侧应防抖或串行执行,不能无间隔递归重试。

class ClipboardWriteQueue {
  private tail: Promise<void> = Promise.resolve();

  enqueue(action: () => Promise<void>): Promise<void> {
    const current = this.tail.then(action, action);
    this.tail = current.catch(() => undefined);
    return current;
  }
}

队列保证应用自身的写入顺序,但无法控制其他应用。系统忙时给用户明确反馈,稍后由用户重新操作,比后台不断覆盖剪贴板更符合预期。

12. 不同设备形态的读取规则需要单独确认

手机、平板、PC/2in1 与原子化服务的剪贴板权限和交互策略并不完全相同。例如鸿蒙电脑应用读取剪贴板可能涉及 ohos.permission.READ_PASTEBOARD。发布前应以目标设备当前官方文档、系统能力和审核要求为准。

{
  "requestPermissions": [
    {
      "name": "ohos.permission.READ_PASTEBOARD",
      "reason": "$string:read_pasteboard_reason"
    }
  ]
}

不要为了兼容所有设备无条件声明权限。只有目标设备和真实功能需要时才添加,并给出用户能理解的用途说明。

13. 页面反馈要区分复制成功和内容可用

setData() 成功表示数据已写入系统服务,不表示任意目标应用都能理解该 MIME、访问 URI 或保留富文本样式。页面提示可以写“已复制”,不要承诺“所有应用都能打开”。

写入成功 -> 提示“已复制到剪贴板”
类型不支持 -> 提示“当前内容无法粘贴到此编辑器”
URI 无权限 -> 引导用户通过系统分享入口发送文件
系统正忙 -> 保留按钮,让用户稍后重试
敏感内容过期 -> 清理本应用写入的数据并关闭会话

这组状态能让用户知道下一步,而不是把所有异常都折叠成“复制失败”。

14. 常见问题按传播边界排查

现象 优先核对 处理方式
其他应用读不到 ShareOption 是否为 INAPP 公开内容改为 LOCALDEVICE
富文本只剩纯文字 接收方是否支持 HTML MIME 同时提供纯文本降级
文件 URI 无法打开 是否只有路径而没有访问能力 使用规范文件分享链路
复制后敏感信息长期存在 是否定义失效时机 会话完成后核对并清理
连续点击偶发报错 应用写入是否并发 加入防抖或串行队列
PC 读取失败 设备权限是否配置 按目标设备文档申请最小权限

先查类型和分享范围,再查权限与接收方能力。不要因为一次粘贴失败就把数据降级为无类型的大字符串。

15. 发布前清单与资料索引

[ ] 每个复制入口都由用户明确触发
[ ] 写入内容只包含接收方真正需要的字段
[ ] MIME 与值类型一致,富文本提供纯文本降级
[ ] 每份 PasteData 显式设置 INAPP 或 LOCALDEVICE
[ ] 读取前调用 hasData 与 hasType
[ ] URI 不包含令牌、内部密钥或未授权沙箱路径
[ ] 一次性敏感内容有会话结束和安全清理策略
[ ] 应用自身写操作已防抖或串行化
[ ] 设备权限只按真实目标形态申请
[ ] 页面文案不夸大接收应用的兼容能力

资料来源:

剪贴板安全不是在复制按钮外再包一层 try/catch,而是从数据最小集、MIME 语义、传播范围、读取校验到失效清理形成完整责任链。每次写入都能解释“为什么复制、谁能读取、何时失效”,系统级共享通道才不会变成无意的数据出口。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐