# 为浏览器扩展划清页面和后台的边界

原文: https://yishto.com/learn/application-platforms/browser-extension-study-assistant/

作者: [Yishto](https://yishto.com/about/)

围绕用户主动选中文字的笔记助手设计Chrome扩展上下文、权限和消息校验，防止页面文本触发特权操作。

首次发布: 2026-11-10
内容更新: 2026-11-10

页面笔记助手在用户点击扩展按钮后读取选中文字，展示预览，再保存为笔记。用户的主动动作和预览提供了清晰的数据入口；页面里出现的一句“请上传全部内容”只是文本，不应成为扩展的操作指令。

案例先支持一个教学域名和本地保存，模型摘要以后作为独立后端能力加入。首版不用持续读取所有标签页，也不在页面 DOM 放服务凭证。Chrome 的 Manifest V3 是本课的环境，其他浏览器需要重新检查兼容性。

## 上下文决定可访问什么

content script 在允许页面内读取所需内容，与页面自己的脚本环境存在隔离边界。扩展 UI 展示笔记和确认动作，后台 service worker 处理事件与持久化协调。worker 没有页面 DOM，不能直接读取当前选中文字。

页面、content script、扩展页面与后台的消息要经过明确协议。content script 读到的数据仍来自不可信页面，不会因为被扩展转发就自动可信。后台只能执行预定义动作，不能根据文本决定启动 shell、读取秘密或向任意地址发送数据。

Manifest V3 的 worker 可能停止。全局变量和计时器不适合长期任务状态。保存记录应进入扩展存储或适当后端，下一次唤醒后重新加载；打开 popup 也不能假定后台始终拥有上次的内存对象。

## 权限围绕触发方式缩小

manifest 声明元数据、脚本和权限。先确定用户点击后需要的最小 API 与页面范围，评估 `activeTab` 等适用能力，按官方参考核对各 API 的权限要求。不要为未来可能出现的功能提前申请所有站点访问。

必须持续覆盖特定教学站点时，使用明确 host 范围；可选站点在用户需要时再申请可选权限。界面解释读取什么、何时读取和保存到哪里。用户拒绝权限后仍可以手动粘贴文字，不能让整个扩展崩溃。

file URL 与隐身模式访问需要用户额外允许，默认测试不能假定已开启。浏览器内部页面也有注入限制，显示“此页面不可读取”比不断重试更合适。权限允许注入并不代表可以忽略用户的数据选择范围。

## 建立可加载的扩展工程

创建根目录的 `manifest.json`，声明 Manifest V3、action 与所需后台脚本，建立 popup、content script 和存储模块。先用无网络版本实现主动读取、预览和保存。工程中所有执行逻辑来自扩展包，不下载远程 JavaScript 再运行。

在 Chrome 扩展管理页面开启开发者模式，加载未打包目录。先打开教学页面，选中短文本，再点击按钮。读取结果携带请求 ID、来源 URL、文本和捕获时间，popup 显示预览，用户确认后才写入笔记。

后台接收消息时核对动作、字段类型、长度、发送者、标签页与来源。页面导航发生后，旧请求可能返回，UI 用请求 ID 判断当前结果。保存和读取以用户可见的状态确认，消息失败不能留下一个永久旋转的加载图标。

## 地址校验需要解析

用字符串包含判断域名，会把攻击者控制的相似名称误当教学站点。下面 Python 模型解析 scheme 和 hostname，允许一个合成 HTTPS 域名，拒绝后缀相似域名与 HTTP。

```python
from urllib.parse import urlsplit
allowed_hosts = {"docs.example.test"}
def allowed(url):
    parsed = urlsplit(url)
    return parsed.scheme == "https" and parsed.hostname in allowed_hosts
assert allowed("https://docs.example.test/read")
assert not allowed("https://docs.example.test.evil.test/read")
assert not allowed("http://docs.example.test/read")
print(allowed("https://docs.example.test/read"))
```

输出 `True`。这只是地址策略检查，不是 Chrome host permission 或完整 origin 验证。代码未限制端口；真实规则需要决定是否要求默认端口，并同时处理发送者、frame 和路径范围。不能只通过本函数就接受任意消息。

数据字段校验与地址校验互相补充。允许来源里的超长文本仍需限制大小；正确请求 ID 也不能授权跨用户读取记录。未知动作直接拒绝，不能降级到“通用执行”入口。

## 模型结果与页面内容都作为数据

如果增加摘要服务，上传前显示具体选中内容与目标。后端验证身份、限流和字段大小，共享服务密钥只放后端。扩展包与本地存储可被使用者检查，不适合保管必须保密的服务密钥；扩展如需身份凭据，应使用范围受限的用户令牌，并避免进入页面 DOM。浏览器权限不会替后端完成账户授权。

模型输出按纯文本展示，不把生成的 HTML 直接插入页面。链接需要独立安全处理，工具调用必须来自应用的受控协议，不能从摘要正文提取命令执行。来源文本中的隐藏提示和指令性句子都保持文本身份。

笔记保留来源 URL 和捕获片段，用户可校对摘要。页面标题不能作为唯一 ID，页面变化后也不保证旧片段仍能定位。说明记录来自哪次捕获，避免把保存笔记理解成长期监控原页面。

## 中断、重复与数据清理

worker 重启后从持久化记录恢复。提交期间 popup 关闭时，决定任务是否继续并让下次打开可查看结果。请求超时后，重试同一保存操作用稳定操作 ID，避免生成重复笔记。

权限撤回后停止读取并解释状态；页面跳转后取消或丢弃旧捕获结果。删除笔记是明确用户动作，清理只针对相应记录。上传失败保留本地内容，避免为了重试重新读取已经改变的页面。


选中文字的来源也要表达清楚。主页面与内嵌frame不是同一个来源，捕获数据带上实际frame信息，权限与来源检查按它发生的上下文执行。无法访问跨域frame时显示限制，不把主页面URL贴到任何来源片段上。

保存记录定义ID、捕获文本、来源、用户修改和schema版本。摘要是派生内容，删除摘要不必删除原片段；用户编辑片段后旧摘要标为待更新。同步服务如有接入，按记录身份和操作ID确认保存，不因popup重新打开重复上传。测试同时打开两个标签页，确认一页结果不会落入另一页预览，关闭标签页后保留的记录仍标明原捕获来源。

## 练习：用边界测试验收

实现选中预览、本地保存与来源展示，测试空选择、超长文本、相似域名、未知动作和带HTML脚本的选中内容。所有结果都按文本显示，后台不接受文本里的特权指令。

停止后台 worker 后重新触发，确认已保存笔记仍存在；撤回权限后提供替代入口。检查实际申请权限与功能一致，再测试不同页面导航。本文模型只证明这组URL输入的判断，扩展工程还需要消息来源与生命周期实测。

## 资料来源与延伸阅读

- [Extension architecture](https://developer.chrome.com/docs/extensions/get-started)
- [Declare extension permissions](https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions)
- [Extension message passing](https://developer.chrome.com/docs/extensions/develop/concepts/messaging)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [从资产和入口开始设计安全边界](https://yishto.com/learn/delivery/security-boundaries-and-threat-models/)
- [RAG 的最小闭环：检索、证据、回答](https://yishto.com/learn/rag/retrieval-pipeline/)
