先记一句能核对的结论:阅读页第一次打开,应当只加载壳页面,还不向服务器取密文。点「打开并查看」之后,Network 里才会出现按编号取密文的请求;成功且次数用尽后,再请求同一编号应看到 410。地址栏可以有 # 后面的密钥,请求行里不该出现同一段。
链接还在,不等于密文还在
「阅后即焚」常被理解成:链接着火了,内容就没了。这个比喻容易说反。聊天软件里的蓝色文字不会自己消失;浏览器历史、邮件原文、工单评论也还在。烧掉的是服务器上那一段暂存的密文。人手里的完整 URL 仍是一串字符,只是再打开时,编号对应的对象已经不在了。
所以要先拆开两件东西。一件是定位凭证:问号后面的编号,用来告诉服务器取哪一条记录。一件是解密凭证:井号后面的密钥,只留给当前标签页。服务器按设计能看见前者,看不见后者。读完一次之后,前者可能还指向一个已经空掉的位置;后者如果还在聊天记录里,也解不开任何东西——因为密文已经没了。
这和网盘分享链接不同。网盘链接有效,文件通常还在;你删的是权限,不是对象本身。阅后即焚把「读」和「删」绑在同一次取回上。创建时可以设定阅读次数(1–10,默认 1)和过期时间(1 小时、24 小时、7 天,或只在阅读后焚毁)。默认路径是:第一次成功取回,密文从服务器删除。未读而过期,同样删除。无论哪种,都没有明文备份可找回。
服务器上从来就没有明文
「读完才删明文」是另一种常见误读。正确顺序是:明文只在创建方的标签页出现;浏览器用 Web Crypto 抽出 256 位随机密钥,按 AES-256-GCM 加密;出站字段是密文、过期时间和最大阅读次数。服务器返回一个不可猜测的编号。页面再把密钥接到 s.html?id={编号}#{密钥} 的片段上。全程没有「先把口令存进数据库再加密」这一步。
算法参数可以当场对照,不是口号。NIST SP 800-38D 建议 GCM 使用 96 位(12 字节)IV,以兼顾互操作和实现简单;认证标签常用 128 位(16 字节)。Web Crypto 的 AesGcmParams 与此对齐。MakePwd 创建时的密文形态是 Base64(12 字节 IV + 密文 + 16 字节标签),密钥 32 字节,单条上限 32 KB。这些数字写在创建页上,也可以在开发者工具里核对自己刚生成的链接:井号后是密钥的 Base64URL,不是你刚输入的那句口令。
GCM 还提供完整性。密文或标签被改过,本机解密会失败,不会给出一段「看起来差不多」的明文。这挡的是传输中的篡改,不是「对方打开后截图」。服务器自始至终拿不到密钥,也就不能代为解密、不能做内容审核、不能在你读完之后再补一份明文副本。它能做的只有:按编号交出密文,然后按次数或 TTL 删掉它。
| 阶段 | 浏览器里有什么 | 服务器上有什么 |
|---|---|---|
| 创建完成 | 明文(你刚输入的)、完整链接 | 密文、编号、TTL、剩余次数 |
| 阅读页刚打开 | 地址栏里的编号和 # 密钥 |
仍是密文;尚未因这次打开而删除 |
| 点确认并取回成功 | 本机解密出的明文 | 次数用尽则密文删除;未用尽则次数减一 |
| 再次打开同一编号 | 密钥可能还在地址栏 | 已焚毁或已过期,返回 410 一类状态 |
编号走问号,密钥走井号
一次性链接必须同时完成两件事:告诉服务器取哪一条,以及告诉接收方浏览器用哪把钥匙。这两件事不该挤进同一条 HTTP 字段。问号后面的查询会写进请求行,反向代理和访问日志按设计都能看见;井号后面的片段按规范留给客户端。
RFC 9110 第 7.1 节写明:目标 URI 不含 fragment,因为片段标识留给客户端处理。浏览器向服务器要的是 s.html?id=…,不是整串地址栏。密钥若改成 ?key=,所谓「只存密文」在日志这一层就不成立:运维打开当天的 access log,就能看到解密所需的第二半。
这一层在上一篇已经展开过,这里只收成边界:拆开是为了让托管方看不见密钥,不是为了让完整链接可以随便转发。把含 # 的整串贴进聊天,接收方和聊天服务商都看得到密钥。fragment 挡的是 HTTP,挡不住剪贴板。细节和当场核对步骤见URL 的井号后面为什么适合放密钥,以及什么时候会失效。
取回密文的那一次,才是焚毁点
「打开阅读页」和「消耗一次阅读」不是同一事件。阅读页的 HTML 可以先画出来:核对地址里有没有 ?id=,有没有 # 密钥,然后停在确认按钮上。这一步只消耗静态资源。真正让服务器删密文的,是随后那次按编号取密文的请求。点了按钮、取回成功、次数用尽,记录才从「仍可取」变成「已焚毁」。
少密钥时不该去取。地址里只有编号、井号被聊天软件截断,阅读页应提示缺少片段,而不是先向服务器要密文。否则你会浪费仅有的一次:服务器交出密文并删除,浏览器却解不开,发送方还以为对方已经读到了。可核对的行为是:缺 # 时,Network 里不应出现取密文的接口;有完整链接并点确认后,才应出现。
次数不是永远等于 1。创建时可以设 1 到 10。设成 3,意味着前两次成功取回后,服务器上的密文还在,只是剩余次数减一;第三次成功后才删除。默认 1 是为了「传一次口令」这个任务,不是因为协议只能读一次。TTL 是另一条独立的删除条件:24 小时到期未读,密文同样删掉,状态应能和「已被人读过」区分开。MakePwd 的阅读页在 410 响应里用 expired 表示过期,其余焚毁态按已烧毁处理。
不要用真实口令、生产环境 API Key 或未脱敏连接串做实验。准备一句可丢弃的金丝雀,例如 canary-burn-2026-do-not-reuse。核对的是状态码和请求行,不是把秘密再传播一遍。
410 和 404:烧掉、过期、本来就没有
密文删掉之后,服务器必须回答下一次请求。RFC 9110 第 15.5.11 节把 410 Gone 写成:目标资源在源服务器上已不再可用,且这一状态很可能永久。若源服务器无法判断是否永久,应当改用 404。MDN 对 410 的说明补充:客户端不该反复重试;站点应撤掉仍指向该资源的链接。
用在阅后即焚上,410 比 404 更贴切:这个编号曾经对应过一条密文,现在被故意删掉了,不会再回来。过期和读尽都可以走 410,再用响应正文区分 expired 与普通焚毁,避免接收方以为「链接写错了」。404 仍然可能出现,例如编号格式无效、记录已被清理到连「曾经存在」都不再声明。对发送方来说,这两种都意味着:不要指望从服务器把内容捞回来。
410 还有一层容易忽略的语义:它告诉爬虫「别再来了」。阅读页本身是临时密文场景,应对搜索引擎 noindex,也不该进 sitemap。你不会希望一次性口令页被收录。创建页可以收录,因为它解释怎么生成链接;阅读落地页只服务持有完整 URL 的人。
聊天预览为什么会先烧掉一次
一次性链接最常见的事故,不是密码学家破了 AES,而是预览机器人比同事手快。你把完整 URL 贴进即时通讯,软件为了画出卡片,会先向这个地址发一次请求。Slack 的链接展开文档写得很直:默认情况下,消息里出现链接时,Slack 会抓取它并提供预览。Teams、Discord、部分邮件安全扫描也有同类行为。它们要的是标题和摘要,不是你的口令;但若「第一次 GET 就取密文并删除」,这次抓取就会把仅有的一次用掉。同事再点进去,看到的是已被焚毁。
密钥在 # 后面,对这类服务器端抓取是一层保护:机器人请求到的是 s.html?id=…,井号后的密钥按规范不随 HTTP 发出。预览通常解不开明文,卡片上也没有口令。它烧掉的是取回次数,不是密钥本身。如果你的实现在加载阅读页时就自动取密文,预览等于替接收方点了一次「打开」。接收方拿到的是 410,发送方以为对方已经读过。
可执行的对策不是禁止贴链接,而是把「画页面」和「取密文」拆开。阅读页先渲染确认界面,说明这次点击会消耗次数;等人动手,再发取回请求。只抓 HTML、不执行按钮点击的预览,会停在确认步。这挡不住执行完整脚本并模拟点击的扫描器,也挡不住人自己点错。它把最常见的「卡片先打开一次」从默认失败改成默认存活。
另一条实践是:在会自动展开的频道里,先发「口令走阅后即焚,链接另发」,或把完整链接放到不预览的私信。完整 URL 仍然是持有即用的凭证,预览问题只是额外消耗次数,不改变「谁拿到整串谁就能解密」这一层。
当场核对:先停在确认页,再搜 Network
口号写「阅后即焚、零知识」无法自证。能当场看到的是四件事:创建时出站有没有明文;阅读页第一次加载有没有取密文;点确认后请求行有没有密钥;次数用尽后再打开是不是 410。
先准备金丝雀。打开阅后即焚创建页,输入一句可丢弃的测试句,TTL 选 1 小时,阅读次数保持 1。生成后不要发给任何人。看结果区的拆分:查询一侧只有编号,片段一侧才是密钥。然后打开开发者工具 Network,勾选 Preserve log,用测试句全文搜索。创建请求的 body 应是密文,不应出现那句原文;分析上报同样不该出现原文。
- 用同一条完整链接打开阅读页,先不要点确认。Network 里应只有文档和静态资源,不应出现按编号取密文的接口。
- 对照地址栏和文档请求的请求行。地址栏保留
#及之后;请求行只应看到 path 和?id=。 - 点确认。此时才应出现取密文的请求;成功后页面显示明文。再用同一链接打开第二次,应进入已焚毁或 410,而不是再给出同一句金丝雀。
三次都符合,只能支持很窄的结论:在你使用的这个浏览器、这一次操作里,明文没有作为已观察到的业务字段出站,密钥没有进请求行,密文在一次成功取回后不再可取。它不证明扩展程序没有读输入框,也不证明服务器磁盘被攻破后历史密文绝对不存在。换浏览器之后,值得再跑一遍金丝雀。若还要核对「任意工具页的明文有没有离开本机」,步骤见浏览器里做加密,怎么当场核对明文没有离开本机。
它拦不住复制、截图和完整链接外发
阅后即焚减少的是两类风险:服务器长期保存明文,以及同一条密文被反复打开。它不减少第三类:接收方已经看见明文之后做什么。对方可以复制、截图、转发、读给旁边的人听。链接本身也可以被完整转发——那时密钥和编号一起走,下一个人同样能在阅读页解密,只要次数还没用尽。
这也是为什么「对方必须登录才能打开」解决不了交接问题。访问控制就是完整 URL。给阅读页加账号,并不能让服务器看不见密钥——密钥本来就不该发给服务器;它只会让接收方多一道门,并把「谁持有链接」改成「谁拥有账号」。MakePwd 没有账号和密码库。创建页和阅读页都打开即用,阅读页对接收方公开。
容量也有边界。32 KB 够放口令、API Key、恢复码和一小段说明,不够放数据库转储或整份证书包。整份文件应在本机做成 .lock / .enc,走网盘或邮件;文件加密盒单文件上限 5 GB,同样打开即用,明文默认不上传。把大文件塞进阅后即焚,不是「更安全」,只是超出这条短文本通道的设计。
用打开即用的创建页把焚毁态练熟
若你希望用一个把拆分写在结果区的工具来练习,可以从 MakePwd 的阅后即焚开始。打开即可使用,双方都不用注册。创建时加密发生在当前标签页,算法是 AES-256-GCM;服务器只收到密文、ttl_hours(0–168,默认 24)和 max_reads(1–10,默认 1)。生成的链接形态固定为 s.html?id={编号}#{密钥}。阅读页先停在确认步,再取密文、在本机解密。
练习时用上面的金丝雀,阅读次数保持 1。跑完同时看三处:创建结果区的查询 / 片段拆分,阅读页第一次加载时 Network 有没有提前取密文,以及第二次打开是否进入焚毁。拆分用来确认密钥没进问号;第一次加载用来确认预览不会误烧;第二次打开用来确认「读完即焚」不是文案。
若内容还要先去掉追踪参数再发出去,先走隐私清洗,再把真正需要保密的短文本放进阅后即焚。清洗解决的是查询串里的 UTM 和点击 ID,不替代一次性密文通道。这些步骤都不要求登录,也不展示未接入的客服邮箱。
常见问题
打开一次后,服务器上还剩明文吗?
从来就没有明文。创建时浏览器先加密,服务器只收到密文。成功取回并达到设定次数后,密文被删除。再请求同一编号会看到 410 或已被焚毁。链接字符串可能还在聊天记录里,那不是服务器上的明文副本。
聊天预览会不会先把内容烧掉?
会烧掉次数,通常烧不掉明文。预览抓的是页面,密钥在 # 后不进 HTTP。若阅读页在加载时就取密文,预览会消耗仅有的一次。正确实现是先确认再取回。Slack 等产品会默认抓取消息里的链接以生成预览。
410 和 404 有什么差别?
410 表示这个资源曾经可用,现在被永久去掉,客户端不该重试。404 更含糊,可能是编号无效,也可能是记录已被清掉。阅后即焚用 410 区分焚毁和过期更清楚。无论哪一种,都没有明文可恢复。
打开阅读页需要登录吗?
不需要。创建和阅读都打开即用。接收方靠编号取密文,靠井号里的密钥在本机解密。MakePwd 没有账号和密码库。访问控制就是完整链接本身。
下次传一次性秘密时记住的三件事
第一,问「读完还剩什么」时,先问服务器上有没有过明文。没有过,删的就是密文;链接还在聊天里,解不开任何东西。第二,焚毁点是取回密文的那一次请求,不是阅读页的第一次绘制。确认按钮挡的是预览机器人,不是已经拿到整串的人。第三,核对看 Network:创建时搜金丝雀原文,阅读时对照请求行与 #,第二次打开看 410。
若你还要继续追问密钥为什么可以放在井号后面,请读URL 的井号后面为什么适合放密钥,以及什么时候会失效。本文只把「读完一次之后,服务端还剩什么」收成可以对照状态码和请求时机的范围。