
1. 需求拆解所谓“保密文章”到底要挡住谁用 vuepress-theme-vdoing 搭博客写到大半年之后几乎每个人都会遇到同一个场景手头有几篇东西不适合公开比如内部项目的技术方案、还没对外发布的复盘记录、带点客户信息的排期表甚至只是自己不想让同事看到的吐槽。这时候第一反应是把文章从docs里挪出去第二反应是——能不能加个密码vdoing 主题的保密文章能力本质上就是回答这个问题。它做的事情很朴素在你写的 Markdown 页面外面套一层密码校验密码对了才把正文渲染出来。听起来简单但我见过太多人配置完之后以为自己“加密”了结果用curl一抓正文原封不动躺在 HTML 里。这篇文章我把整条链路拆开讲vdoing 的 lock 字段到底做了什么、Vue 这一层能配置哪些东西去接管访问控制、配置写完之后怎么验证才敢说自己真的锁上了以及那些年我在静态站点上踩过的泄露坑。适合两类人看一是刚上手 vdoing、想给自己几篇私密文章加把锁的新手二是有一定前端基础、想知道“前端加密”这条路的边界在哪、能不能用在正经业务里的开发者。读完你至少能做到三件事把一篇保密文章正确配置出来、知道去构建产物里查什么、判断自己的场景该不该用这套方案。1.1 静态博客天生没有门禁VuePress 出来的东西本质就是一堆静态文件。npm run build之后dist目录里的每一个.html、每一段 JS、每一个 JSON 都是公开可下载的资源服务器只负责按路径把文件递给你它不知道你是谁、也不关心你是谁。这一点和带后端的系统有根本区别后端可以做到“请求到我这儿我先查你的身份不给你数据你就什么也拿不到”静态站点做不到文件一旦部署出去路径对了就能拿到。所以任何在静态站点上做的“保密”本质上都是客户端自律内容已经送到浏览器了只是暂时不让你看见等你证明自己知道密码再展示。这句话不是我泼冷水而是所有后续决策的基础。你只有先承认这一点才会认真对待后面那几个验证步骤而不是配完lock: true就心安理得地往仓库里推。打个生活化的比方静态站点的加密像是在快递箱外面贴了个封条。封条撕开了里面的东西就在那儿它挡的是“顺手看一眼”的人挡不住“拿刀划开”的人。而真正的后端鉴权是快递员必须核对身份证才把箱子交给你箱子里根本没东西可偷。两者不是替代关系是成本不同的两种方案。1.2 三条路线与它们的真实成本在动手配置之前先把自己想要的效果对齐一下不同诉求对应完全不同的实现路线。我把常见的三种整理成了下面这张表你可以对号入座。注意这里的“成本”不光是金钱更多是维护心智和后续变更的麻烦程度。路线核心做法能挡住谁主要代价适合场景前端遮罩式主题自带 lock 字段客户端校验密码后渲染普通访客、搜索引擎的常规抓取内容仍在产物中懂技术的人可绕过个人博客、非敏感的内部备忘路由层拦截Vue Router 守卫 登录态未通过统一跳转直接输 URL 的访客、站内导航需要额外维护登录页与状态正文仍可能被构建进页面小团队内部文档、半公开资料构建期隔离私密内容不进公开仓库独立构建、独立部署绝大多数人要维护两套站点或一套 CI 流程真敏感内容、有明确边界的内网资料大部分人的实际需求落在第一行因为“不想被随手翻到”和“防止被窃取”是两码事。但如果你手上的是客户数据、没发布的商业计划这类东西请直接看第三行别在前端加密上浪费时间。这里有个判断标准我个人一直在用假设这篇内容明天出现在搜索引擎快照里你能接受吗能接受前端加密够了不能接受就必须走构建期隔离或者服务端鉴权。这个自问自答能省掉后面 90% 的纠结因为很多人真正需要的其实是“别让同事在站内导航里点到”而不是“抵抗有心人”。2. vdoing 的 lock 是怎么跑起来的搞清楚原理比背配置项重要得多因为 vdoing 在不同大版本之间这块的字段名和实现方式是有差异的别人博客里抄来的写法在你的环境里可能根本不生效而你还以为锁上了。我的一般做法是先用官方文档的示例跑通一遍然后去node_modules里把相关源码翻出来看两眼确认数据流最后才敢下结论。2.1 从 frontmatter 到客户端解密的链路整条链路大致是这样四步。第一步你在某个 Markdown 文件顶部的 frontmatter 里写了锁相关的字段比如--- title: 内部方案复盘 date: 2024-05-11 lock: true password: your-pass-here ---注意lock和password的具体写法在不同版本里可能是lock: 密码串、也可能是lock: true配一个单独的字段还可能是统一在themeConfig里配置。我不建议你直接照抄任何一篇博客包括这篇正确姿势是打开node_modules/vuepress-theme-vdoing/目录全局搜索lock这个关键字看看它读取的是$page.frontmatter里的哪个字段名。这一步花不了三分钟但能避免你后面一小时的自我怀疑。第二步构建阶段主题的插件或编译钩子识别到这篇页面带锁会对正文做一次处理。这个处理可能是把内容加密后塞进页面数据也可能是保留原文但加一个渲染开关两种做法在安全性上天差地别也是我后面反复强调要去产物里验证的原因。第三步浏览器加载页面主题的客户端组件接管渲染先显示一个密码输入框把正文挡在后面。第四步用户输入密码客户端比对比对通过后把正文渲染出来通常还会往localStorage里写一个标记这样同一次会话内不用反复输入。// 大致是这个意思具体键名以源码为准 localStorage.setItem(lock- pageKey, unlocked)理解这条链路之后你就能自己回答一个关键问题密码比对是在前端做的那么密码本身是不是也在前端如果在那它就是一个“约定暗号”而不是“密钥”。这个认知直接影响你对这套方案信任度的判断。2.2 先验证再谈安全三分钟看清产物里有什么我最想强调的一步也是绝大多数教程略过的一步构建之后去产物里搜一搜。方法很简单先把站点构建出来npm run build然后拿一句你文章里独有的、不常见的话去搜比如你文中写了一句“这个方案我们最终砍掉了三版”grep -rn 最终砍掉了三版 docs/.vuepress/dist/ | head -20如果搜出来了而且出现在某个.html或者.js里那说明正文是明文进产物的你现在的“加密”只是前端遮罩任何人查看源码就能读到。这种情况对个人博客完全可以接受但你得知道自己处于什么状态而不是误以为安全等级更高。如果搜不到说明主题确实做了内容层的加密处理安全性上了一个台阶但也不代表绝对安全密钥分发仍然是个问题。再补一个验证角度打开构建后的页面在浏览器里禁用 JavaScript 再看一眼。有些实现是服务端渲染时就把正文输出到了 HTML客户端只是把它藏起来比如display: none禁 JS 之后反而全露出来了。这个检查花十秒钟但能揪出很隐蔽的问题。提示验证时不要只看首页或者你自己测试的那个链接把dist目录里所有文件都搜一遍包括那些带哈希的 JS 资源包和搜索索引文件。2.3 用 Vue 配置接管三个可以落手的入口标题里说的“通过 Vue 配置”指的就是跳出主题自带的 lock 字段用 VuePress 暴露的 Vue 层扩展点自己接管访问控制。vdoing 基于 VuePress 1.x能落手的地方主要有三个我按侵入性从低到高排列。第一个入口是themeConfig也就是docs/.vuepress/config.js里的主题配置对象。这里能做的是“别让它出现在该出现的地方”导航栏里不给链接、侧边栏不自动收录那个目录、首页文章列表不把它算进去。这属于“藏”不是“锁”但两者经常需要配合因为很多人以为加了密码就没人能找到结果首页归档列表把标题和摘要全亮出来了。第二个入口是enhanceApp.js这是 VuePress 1.x 标准的客户端应用增强文件在docs/.vuepress/下新建导出一个函数能拿到Vue、router、siteData等对象。在这里注册全局路由守卫是最常见的手段可以针对某个路径前缀做统一拦截也能顺便埋点统计谁试图访问私密区。第三个入口是全局 mixin。通过Vue.mixin()注入一段逻辑可以在每个页面组件的生命周期里做统一的鉴权或水印处理。这个手段比较重会作用到所有页面一般只在需要“全站级”的策略时才用比如给所有私密页面的 DOM 加一层防截屏的水印文字。这三个入口的能力边界要分清themeConfig管的是信息暴露面enhanceApp管的是访问路径mixin管的是页面内部行为。做保密文章这件事前两个基本够用第三个属于锦上添花先别急着上。3. 实操把一篇保密文章从零跑通下面这部分是完整的落地过程我按真实操作的顺序写包括每一步的意图和判断依据。你可以照着抄但请务必把示例里的路径、密码、字段名替换成你环境里真实存在的值尤其是字段名一定要先按 2.1 节的方法确认过。3.1 目录与导航的前置规划动手写配置之前先把目录结构定下来。我推荐的形态是把私密内容集中放在一个独立目录比如docs/private/而不是散落在各个分类下面。原因有三个一是路径前缀统一之后路由守卫用一条正则就能覆盖不用维护一长串白名单二是这个目录可以整体在构建脚本里做条件剔除未来想升级到“构建期隔离”路线时改动最小三是心理上的边界清晰你在写新文章时能立刻判断该放哪边。docs/ ├── .vuepress/ │ ├── config.js │ ├── enhanceApp.js │ └── public/ ├── private/ │ ├── plan-2024.md │ └── review-q2.md ├── technology/ └── README.md配套要考虑的是导航。vdoing 的侧边栏可以配置成自动根据目录结构生成这意味着只要docs/private/存在它大概率会自动出现在侧边栏里标题一览无余。你有两个选择一是把它从侧边栏配置里显式排除二是接受标题可见、内容锁住。我一般选第一个因为标题本身就经常泄露信息比如“Q2 裁员沟通口径”这种标题锁不锁内容意义已经不大了。// docs/.vuepress/config.js 片段 module.exports { themeConfig: { nav: [ { text: 首页, link: / }, { text: 技术, link: /technology/ } // 注意这里不要放 private 的入口 ], sidebar: structuring // 结构化侧边栏会按目录自动生成需确认是否会收录 private } }如果你的主题版本支持在 frontmatter 里用某个字段把页面排除出列表和侧边栏那就更省事直接给每篇私密文章加上标记即可。这个字段名同样建议去源码里确认常见的候选是article、sidebar、pageComponent之类的布尔开关。确认不了也没关系退一步用手工维护侧边栏虽然麻烦但绝对可控。3.2 配置文件逐段落地先写文章本身。私密目录里的一篇 Markdownfrontmatter 除了常规的title和date还要带上锁相关字段。这里我把两篇文章都写成示例方便你对照--- title: 内部方案复盘 date: 2024-05-11 lock: true password: change-me-please --- 正文从这里开始写完记得做产物验证……然后是enhanceApp.js这是路由层拦截的核心。下面这段代码的意思是当访问路径以/private/开头时检查本地是否存有有效通行证没有就跳到一个提示页。注意isServer判断VuePress 构建时会跑一遍 Node 端的渲染window和localStorage在那时候是不存在的不加判断会直接报错。// docs/.vuepress/enhanceApp.js const PASS_KEY blog-private-token export default ({ router, isServer }) { // 服务端渲染阶段没有 window直接返回 if (isServer) return router.beforeEach((to, from, next) { const isPrivate /^\/private\//.test(to.path) if (!isPrivate) return next() const token window.localStorage.getItem(PASS_KEY) if (token unlocked) return next() // 带上来源路径方便验证通过后跳回去 next({ path: /401.html, query: { from: to.fullPath } }) }) }对应地你需要在docs/401.md建一个提示页。注意 VuePress 1.x 里 Markdown 文件会生成同名 HTML路径是/401.html别写成/401/否则跳转过去是 404用户看到的是“页面不存在”而不是“请先验证”体验会很差这一点我踩过。--- title: 需要验证 sidebar: false --- 这个页面的内容暂时不对外展示。如果你有访问口令请回到原文页面输入。 如果你是从收藏夹直接进来的说明链接失效了请联系站点维护者。至于口令怎么下发、验证通过后怎么写 token如果你用的是主题自带的 lock这部分主题已经处理好了如果是自己实现的路由拦截就需要一个输入框页面验证通过后写入localStorage再跳回from。这里提醒一句localStorage是永久存储除非用户手动清否则一次验证长期有效。如果你希望“关掉浏览器就失效”换成sessionStorage代价是每次重开都要输一遍看你的取舍。如果要做得再精细一点口令不要明文比对至少做个哈希。浏览器原生提供了 Web Crypto不用引入任何库async function sha256(text) { const data new TextEncoder().encode(text) const hash await crypto.subtle.digest(SHA-256, data) return Array.from(new Uint8Array(hash)) .map(b b.toString(16).padStart(2, 0)) .join() }把口令的哈希值写在前端用户输入后算哈希再比对这样源码里至少看不到明文口令。但这里有个真实的坑crypto.subtle只在安全上下文可用也就是 HTTPS 或者localhost。你要是用局域网 IP 直接访问本地起的服务比如http://192.168.x.x:8080这个 API 会返回undefined然后你会看到一堆莫名其妙的报错排查半天以为是打包配置问题。开发阶段要么用localhost要么就老老实实退回明文比对别在本地跟自己较劲。3.3 验收清单刷新、直链、搜索、产物配置写完不等于做完了下面这张清单我每次上线前都会走一遍缺一项都不敢说自己锁上了。表格里“通过标准”那一列是我自己定的你可以按需加严。检查项操作通过标准站内导航首页点各种链接乱逛私密文章标题和摘要都不出现直接访问浏览器手动输/private/plan-2024.html出现验证提示看不到正文刷新页面验证通过后按 F5不需要重新验证或按你的设计重新验证关闭 JS浏览器禁用 JavaScript 后访问仍然看不到正文产物检索grep独有关键句结果符合你的安全预期搜索索引站内搜索框搜私密文章里的词搜不到或明确知道能搜到直链分享把链接发给不知情的朋友对方打不开正文这里重点说两个容易被糊弄过去的。“刷新页面”这一项很多实现是把解锁状态存在组件内部的data里一刷新就回到锁屏用户会以为出 bug 了其实是状态没持久化。“关闭 JS”这一项测的是正文到底有没有被服务端渲染出来前面说过有些实现只是display: none禁 JS 反而原形毕露。“产物检索”那一项我再展开一点。除了搜正文还应该搜一下文件名和标题# 搜正文关键句 grep -rn 最终砍掉了三版 docs/.vuepress/dist/ | head # 搜标题 grep -rln 内部方案复盘 docs/.vuepress/dist/ | head # 看看有没有可疑的搜索索引文件 find docs/.vuepress/dist -name *.json | head -20第三个命令经常有惊喜你会看到一些插件生成的索引 JSON打开看一眼就知道自己的正文有没有被抽进去。这一步我强烈建议做因为搜索索引泄露是我见过最常见、也最容易被忽略的问题。4. 踩坑实录与排查速查表配保密文章这件事报错不多但“以为成功其实没成功”的情况特别多。下面这些是我自己和周围人真实遇到过的问题按出现频率排序前面的最常撞上。4.1 六个高频故障的定位顺序第一个密码输对了还是提示错误。先确认你改的字段名主题真的在读。去源码搜lock看它取的是哪个字段再看有没有全角字符、多余空格YAML 里的引号有没有吃掉首尾空格。我见过一次是密码里带#在 YAML 里被当注释截断了只剩下前半截这种问题肉眼极难发现换一个不含特殊字符的密码立刻就能验证。第二个路由守卫根本不触发。检查enhanceApp.js的路径和文件名VuePress 1.x 认的是docs/.vuepress/enhanceApp.js写成enhance-app.js或者放到子目录里都不会生效而且不报错非常安静。另外注意导出的是一个函数不是对象写错形态也不会报错只是不执行。第三个构建时报window is not defined。典型原因是守卫或者 mixin 里直接用了window没有判断isServer。VuePress 构建时会在 Node 环境跑一遍渲染那里没有window。所有涉及浏览器 API 的代码都必须包在if (!isServer)或者typeof window ! undefined里。第四个首页文章列表依然暴露私密文章。这说明你只做了页面级拦截没做列表级过滤。首页列表的数据源通常是$site.pages全集需要按路径或者 frontmatter 标记过滤掉。主题如果不支持最干净的做法是手工维护列表或者直接放弃列表展示把私密目录当作“只能通过已知链接访问”的区域。第五个站内搜索能搜到加密正文。这个最隐蔽。全文搜索类插件在构建时会把页面正文抽出来做成索引加密逻辑在客户端索引在构建时就生成了两者互不干扰。解决办法要么把私密目录加进搜索插件的排除列表要么干脆关掉全文搜索。我的建议是排除别关关了公开内容的搜索体验会明显下降。第六个改了配置没生效。VuePress 的配置和主题配置在某些情况下需要重启 dev server 才生效尤其是enhanceApp.js这种应用级文件。养成改完就CtrlC再npm run dev的习惯能省掉大量“我明明改了”的困惑。4.2 最容易被忽略的三个泄露口第一个是RSS / Feed 输出。如果你装了生成订阅源的插件它很可能把文章全文塞进 XML 里而那个 XML 是公开可访问的。验证方法很简单构建完成后在dist里找.xml文件打开看你私密文章的正文在不在里面。第二个是站点地图。sitemap.xml会把所有页面的 URL 列出来虽然不含正文但等于给爬虫和好奇的人提供了一份“私密页面清单”。如果私密文章的标题本身就是敏感信息光这一条就够呛。处理方式和搜索索引类似加排除规则。第三个是预览摘要和 og 标签。vdoing 的首页文章列表通常会显示一段摘要这段摘要来自正文的截取也就是说即使正文锁住了摘要照样公开。更麻烦的是社交分享用的 meta 标签很多主题会自动把文章标题和摘要塞进meta propertyog:description你分享链接到群里聊天软件的预览卡片直接把摘要亮出来。这一条我是在一次分享后才发现的当时尴尬得不行。提示这三个泄露口的共同点是“不在正文里但在产物里”。所以验证方法永远是同一招——构建、grep、打开可疑文件看一眼。5. 能力边界与更稳的组合拳聊完怎么配得回到最开始那个问题这套东西到底能用在什么场合。我见过太多人把它用在不该用的地方最后要么是自欺欺人要么是给自己埋了个雷。5.1 前端加密能防什么防不住什么它能防的普通访客随手点进来、站内导航误入、搜索引擎的常规抓取、把链接转发到群里之后大部分人的好奇心。在个人博客、小团队内部备忘这个量级上它的投入产出比非常高配置半小时心理负担立刻下降。它防不住的会看网页源码的人、会翻dist目录的人、会抓包看接口返回的人。只要你把浏览器开发者工具打开Network 面板一看返回的 payload 里有什么一清二楚。所以任何以“绝对不能被看到”为前提的内容都不该走这条路。还有一个常被忽略的边界是合规和追责。如果你在文章里放了不该放的东西比如别人委托你的资料然后你用前端密码“保护”起来了真出了事这个密码在法律意义上不构成任何保护措施。这一点必须想清楚别把技术手段当成免责声明。5.2 分层策略把“挡人”和“藏内容”分开我现在的做法是分层。第一层是公开区随便看第二层是“半公开区”用 vdoing 的 lock 加个口令内容属于“不想让所有人看到但泄露了也不致命”的量级第三层是真正的敏感内容压根不进这个仓库单独一个私有仓库构建产物部署在需要额外访问控制的地方或者干脆只发文件不发站点。这套分层最大的好处是决策变简单了。每次写新文章我只问自己一句“这东西泄露了会怎样”答案直接对应到某一层不用纠结。而且随着时间推移你会发现 95% 的内容其实都在第一层真正需要锁的东西少之又少这反过来也减少了维护成本。如果你确实需要更强的保护可行的升级路径有两条。一是把私密站点和公开站点拆成两个 VuePress 实例共用组件和样式私密那个的产物部署到受限路径下公开的那个完全不引用它。二是在前面加一层反向代理做基础认证静态文件本身不变但访问前要先过一道关。这两条路都比在前端加密上继续加码要实在。最后分享一个我在实际操作中的习惯每次改完保密相关配置我都会用一个没登录过的浏览器窗口无痕模式完整走一遍流程从首页逛到私密文章再从私密文章的直链进一次最后翻一眼构建产物。这三步走完心里才踏实。前面那些坑基本都是“在自己的登录态里测了半天觉得没问题”造成的。