📚 离职知识库

内容付费墙 —— 三种实现方式

这个文件夹整理了三种「把正文锁起来、换取某种代价才放行」的实现方式,用于以后自建站参考。 每种方式各自附一份参考文档和一份 Claude Code Skill。01/02 来自对真实站点的逆向观察,03 是我们自研并逐字节验证过的方案。

#三种方式速览

方式一:服务端硬墙 方式二:前端软墙 方式三:客户端加密墙
子文件夹 01-服务端硬墙-会员API/ 02-前端软墙-公众号涨粉解锁/ 03-客户端加密墙-密码解锁/
原型 best.xiaohu.ai(AI 解读站) xiaolincoding.com(小林coding) FileBeam(自研)
"货币" 真金白银(会员订阅) 一个公众号关注(涨粉变现) 一个密码 / 密钥(谁有谁能开)
正文放哪 只在服务器,游客拿不到 拦截逻辑全在前端(明文已下发) 公开下发,但是密文
要自己的服务器吗 要(鉴权 API) 不要 不要(静态托管即可)
凭证 服务端签发的 JWT,装 httpOnly Cookie 验证码校验后写的 localStorage 标记 密码/密钥(浏览器本地 WebCrypto 解密)
防护强度 硬(前端偷不到内容) 软(涨粉挡板,理论上可绕) 较硬(没密钥就是乱码,强度=密钥强度)
适合场景 真付费内容、要防白嫖 引流涨粉、软性鼓励关注 无后端也要真锁、卖加密文件、免费 CDN 扛带宽

#怎么选

  • 要真的靠内容收费、内容值钱、必须防白嫖,且愿意自己架后端 → 方式一(硬墙)。核心是「正文永远不下发给没资格的人」。
  • 只是想涨粉/引流,内容本身可以给,只是想换个关注 → 方式二(软墙)。搭起来快、成本低,但别指望它拦得住懂技术的人。
  • 没有 / 不想要后端(纯静态托管),但又要真锁住内容;或想卖"离线也能开的加密文件";或想让免费 CDN 扛正文带宽、自己只护一把钥匙 → 方式三(加密墙)。硬度取决于密码/密钥强度。
  • 组合玩法
    • 外层挂 Cloudflare 挡爬虫 + 内容用硬墙鉴权 = 最稳的自建后端组合。
    • 加密墙的变种 B = 正文密文放免费 CDN + 解密 key 用硬墙鉴权接口按会员下发 = 硬墙级收费控制 + 静态托管的成本,是三者叠加的甜点区。

#三种方式的本质分界

判断标准:被锁的正文到底以什么形态离开了服务器?

  • 硬墙:没离开(游客 DOM 里根本没有正文)。
  • 软墙:以明文离开(内容已在前端,只被遮罩挡住 → 可绕)。
  • 加密墙:以密文离开(内容发出去了,但没密钥拼不回来 → 下发 ≠ 泄露)。

#每个子文件夹里有什么

  • 参考文档.md:讲清这种方式的完整原理、请求时序、鉴权/加密细节、坑,以及从零搭建的技术选型。
  • SKILL.md:一份写给 Claude Code 的技能说明书。以后把这个 skill 放进项目,跟 Claude Code 说「用这套方式给我的站做付费墙」,它就照着这份蓝图帮你实现。

#方案:01-服务端硬墙-会员API


#name: hard-paywall-membership-api description: 给内容站做"服务端硬墙"付费墙——正文锁在需鉴权的 API 后面,游客永远拿不到全文。当用户要做真付费内容、会员订阅、防白嫖的付费墙,或说"帮我的站加个硬付费墙/会员墙"时使用。

#服务端硬墙付费墙(会员鉴权 API)

这份技能教你给一个内容站搭建服务端硬墙:文章外壳公开(含免费部分 + 付费卡片),完整正文锁在一个需要会员身份的 API 后面。核心铁律:正文永远不下发给没资格的人,前端只负责渲染服务器给的东西。

参考机制详见同目录 参考文档.md(基于对 best.xiaohu.ai 的真实逆向)。

#什么时候用这套

  • 内容值钱、要靠订阅收费、必须防白嫖 → 用这套。
  • 只是想涨粉引流、内容可以送 → 别用这套,用 02-前端软墙 那套,成本低得多。

#架构总览

[Cloudflare 防爬] → [外壳页 SSR/SSG] → [全文鉴权 API] ← [JWT/HttpOnly Cookie] ← [支付回调]

拆成两条内容路径:

  • GET /article/{slug}:公开外壳 = 标题 + 免费部分(服务端截断)+ 付费卡片。给 SEO 和游客。
  • GET /api/full/{slug}:私有全文 = 鉴权通过才返回完整正文 JSON,否则 401。

#实现步骤(按顺序执行)

#1. 数据模型

  • articlesslug, title, free_content, paid_content, ...(免费/付费两段分开存)。
  • usersid, email, ...
  • subscriptionsuser_id, expires_at, status

#2. 外壳页(SSR/SSG)

  • 渲染 title + free_content + 付费卡片
  • 绝不要paid_content 输出到 HTML 里(哪怕隐藏也不行——那就退化成软墙)。
  • 免费部分的截断长度在服务端决定(按段落/字数)。

#3. 全文鉴权 API GET /api/full/{slug}

1. 从请求 Cookie 读 JWT
2. 验签(用服务端密钥,验签不查库)
3. 查 subscriptions:是否会员、是否未过期
4. 通过 → 200 { paid_content }
   不通过 → 401(不要泄露任何正文)

#4. 鉴权凭证

  • 登录/支付成功后 Set-Cookie:JWT + HttpOnly + Secure + SameSite=Lax
  • 不要把 token 放 localStorage(防 XSS)。
  • JWT payload:{ sub: userId, exp: 到期时间 },用 HS256/RS256 签。

#5. 支付闭环

  • 接 Stripe / 支付宝 / 微信支付。
  • 支付成功的 webhook 里写 subscriptions,标记会员 + 到期时间。
  • 前端支付回跳后重新 fetch /api/full/{slug},此时带上会员 Cookie → 200 → 渲染全文。

#6. 前端渲染逻辑

const res = await fetch(`/api/full/${slug}`, { credentials: 'include' })
if (res.ok) {
  const { paid_content } = await res.json()
  renderFullContent(paid_content)      // 替换掉付费卡片
} else {
  // 401:保持付费卡片,引导登录/购买
}

#7. 外层防爬

  • 整站挂 Cloudflare,开 Bot Fight Mode。
  • API 路由加基础限流。

#验收清单(务必逐条自测)

  • 未登录时,查看网页源代码 / 搜索整个 DOM,搜不到付费正文的任何一句。
  • 未登录请求 /api/full/{slug} 返回 401,响应体不含正文。
  • document.cookie读不到 JWT(因为是 HttpOnly)。
  • 免费部分能被搜索引擎抓到(SEO 正常)。
  • 购买后刷新,付费卡片被全文替换。
  • 退款/到期后,同一账号再请求 /api/full 变回 401。

#红线(做错就白做)

  1. 全文绝不能进浏览器(哪怕 CSS 隐藏)——这是硬墙和软墙的唯一分界线。
  2. 鉴权在服务端,前端只是执行结果,别把权限判断交给前端。
  3. JWT 不进 localStorage,只进 HttpOnly Cookie。

#技术选型建议

  • 全栈框架:Next.js(App Router)或 Astro + 一个 API 后端。
  • 数据库/鉴权:Supabase(自带 Auth + Postgres + RLS,省事)。
  • 部署:Vercel + Cloudflare(Vercel 跑站,Cloudflare 做 CDN/防爬)。
  • 支付:Stripe(海外)或支付宝/微信(国内)。

#方案:02-前端软墙-公众号涨粉解锁


#name: soft-paywall-wechat-unlock description: 给内容站做"前端软墙"——正文被前端挡住,关注公众号发验证码即可解锁,用于涨粉引流。当用户要做"关注公众号解锁文章""扫码涨粉墙""软性内容挡板",或说"帮我的站加个公众号解锁"时使用。

#前端软墙付费墙(公众号涨粉解锁)

这份技能教你给内容站加一道前端软墙:正文(或后半段)被挡住,弹二维码让访客关注公众号、发关键词拿验证码、输码解锁。解锁状态写本地。目的是涨粉引流,不是真加密。

参考机制详见同目录 参考文档.md(基于对 xiaolincoding.com 的真实逆向)。

#什么时候用这套

  • 内容可以送、只想换个公众号关注、要的是涨粉 → 用这套,搭起来快、成本低。
  • 内容真值钱、要防白嫖 → 别用这套,用 01-服务端硬墙 那套。软墙拦不住懂技术的人。

#两种变种,先选一个

  • 验证码模式(推荐,最省事):固定二维码 + 手动输验证码。不用为每个访客生成东西,不用轮询。
  • 带参二维码 + 轮询模式:每个访客一个 scene_id,扫码即自动解锁,体验更顺但要后端配合推送 + 前端轮询。

除非明确要"扫完自动解锁"的顺滑体验,否则用验证码模式。

#实现步骤(验证码模式)

#1. 解锁组件(前端预埋)

  • 在文章模板里预埋一个隐藏的解锁弹窗:固定二维码图 + 验证码输入框 + 提交按钮。
  • 正文超过某个位置就截断/遮住,露出「阅读全文」按钮。
  • 点「阅读全文」→ 把弹窗 display 出来(不必等网络)。

#2. 公众号侧(拿验证码)

  • 用户关注公众号后,发送约定关键词(如「验证码」)。
  • 公众号后台(第三方涨粉 SDK 或自建微信消息接口)收到消息(含 openid)→ 生成一次性 code → 自动回复。

#3. 校验解锁

// 用户输入 code 后
const res = await fetch('/verify', {
  method: 'POST',
  body: JSON.stringify({ code })
})
if (res.ok) {
  localStorage.setItem('site-unlocked', '1')   // 永久解锁标记
  revealContent()                               // 拆墙
}
  • 后台校验 code 有效(可选:查该 openid 是否仍在关注)。

#4. 记住解锁

  • 页面加载时先读 localStorage.getItem('site-unlocked'),有标记就直接放行、不弹窗。

#实现步骤(带参二维码 + 轮询模式,可选)

① 前端进页面时向后端要一个带 scene_id 的二维码
② 用户扫码关注 → 微信推 subscribe 事件(scene_id + openid)到后端 → 后端记 scene 已关注
③ 前端每 ~1.5s 轮询 /check?scene=xxx
     pending → 继续等;success → 写 localStorage 标记 + 拆墙
④ 设个轮询上限(如 2 分钟)避免死等

#幕后:接第三方涨粉 SDK 最省事

自己搭微信公众号消息接口(收 openid、发/校验验证码、管 scene)很麻烦。推荐直接接一个第三方"公众号涨粉/阅读全文"SDK:它托管消息回调、验证码/scene 逻辑,还给前端组件。你只要引脚本、绑公众号、配关键词。

#验收清单

  • 首次访问,正文被挡,露出「阅读全文」。
  • 点击后弹出二维码 + 验证码框(无需等网络)。
  • 关注公众号发关键词,能收到自动回复的验证码。
  • 输对验证码后正文放行,localStorage 写入解锁标记。
  • 刷新/再访问不再弹窗(读到本地标记直接放行)。

#老实提醒(别自欺)

  1. 这是软墙:内容通常已在浏览器/静态资源里,禁 JS、读源码、删遮罩都能绕。
  2. 它是涨粉工具,不是加密。真要保护值钱内容,去用硬墙(01 那套)。
  3. 验证码模式够用,别为了体验强上轮询把架构搞复杂,除非确有需要。

#技术选型建议

  • 站点:VitePress / VuePress / Astro / Hexo 任意静态站。
  • 解锁:第三方涨粉 SDK(省心)或自建微信公众号消息接口。
  • 二维码:固定图片托管在 CDN 即可(验证码模式)。
  • 状态:localStorage 一个布尔标记。

#方案:03-客户端加密墙-密码解锁


#name: encrypted-paywall-client-side description: 给内容做"客户端加密墙"——正文 AES 加密后公开托管(静态即可),靠密码/密钥在浏览器本地解锁。当用户要在没有后端/纯静态托管下真锁住内容、卖加密文件、或想让免费 CDN 扛正文带宽只护一把钥匙时使用。说"加密墙/密码解锁/加密后发出去/给内容上锁"时触发。

#客户端加密墙(密码 / 密钥解锁)

这份技能教你给内容做客户端加密墙:把正文用 AES-GCM 加密成密文,连同一段自解密程序一起嵌进页面,托管在任意静态服务上(连免费图床都行)。服务端不参与鉴权也不解密,访问者在自己浏览器里输密码/密钥当场解开。它填补 01 硬墙(需服务器)和 02 软墙(拦不住)之间的空档:静态托管也能真锁住内容

参考机制详见同目录 参考文档.md(基于自研并逐字节验证过的 FileBeam 方案)。

#什么时候用这套

  • 没有 / 不想架后端,但要真锁住内容(硬墙做不到无服务器,软墙锁不住) → 用这套。
  • 想让免费 CDN 白嫖扛大文件带宽,自己只花极小成本做收费 → 用变种 B
  • 要把"带锁的东西"整个交付、离线也能开(卖加密 PDF / 资料包) → 用变种 A
  • 要精细会员/退款/到期控制、且不在乎自己扛正文带宽 → 回去用 01-服务端硬墙

#先选变种

  • 变种 A:静态密码(一个口令走天下)。零后端,一个文件自带锁。缺点:密码人人通用、可转发,强度靠密码本身。适合低值内容 / 一次性交付。
  • 变种 B:密钥托管接口(加密墙 + 硬墙鉴权,★ 真付费首选)。正文密文放免费 CDN,解密 key 锁在鉴权接口后按会员身份下发。安全性等同硬墙,还省正文带宽。

#加密铁律(两个变种通用,做错就白做)

  1. 只用 AES-256-GCM(带认证),别用裸 CBC/CTR——改字节要能被发现。
  2. 口令必须过 PBKDF2/scrypt 慢哈希 + 随机 salt,绝不能 SHA256(密码) 直接当 key。
  3. 每个文件独立随机 salt + iv,iv 绝不复用。
  4. 加密在本机/服务端做,明文不落日志、不落临时文件。

#实现步骤(变种 A:静态密码)

#1. 加密并生成自解密页

用 Node 内置 crypto(无需装库):

import crypto from 'node:crypto';
const salt = crypto.randomBytes(16), iv = crypto.randomBytes(12);
const key = crypto.pbkdf2Sync(password, salt, 150000, 32, 'sha256');
const c = crypto.createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([c.update(plainBuf), c.final()]);
const payload = Buffer.concat([ct, c.getAuthTag()]).toString('base64'); // 密文||tag
// 把 salt/iv/payload/迭代次数 base64 嵌进 HTML 模板

#2. 页面里的解密逻辑(浏览器 WebCrypto,和上面对齐)

const km = await crypto.subtle.importKey('raw', new TextEncoder().encode(pw), 'PBKDF2', false, ['deriveKey']);
const key = await crypto.subtle.deriveKey(
  { name:'PBKDF2', salt, iterations:150000, hash:'SHA-256' },
  km, { name:'AES-GCM', length:256 }, false, ['decrypt']);
const buf = await crypto.subtle.decrypt({ name:'AES-GCM', iv }, key, payload); // 错密码这里直接抛错
// buf → Blob(原始 mime) → 渲染 / 下载

#3. 托管 + 交付

  • 把生成的 HTML 传到任意静态托管(litterbox / GitHub Pages / S3 / R2 / IPFS)。
  • 密码线下发给付费者(收款后私信/邮件)。

直接复用现成实现:~/.claude/skills/send/send_secure.mjs 就是变种 A 的完整可用版(加密任意文件 + 上传)。

#实现步骤(变种 B:密钥托管接口,真付费推荐)

① 加密:生成一把随机 256 位 key,用它 AES-GCM 加密正文(这次 key 不派生自密码,纯随机)
② 分开托管:
   - 密文(大)→ 免费/静态 CDN 公开放,URL 谁都能下
   - key(几十字节)→ 存进你的数据库,别公开
③ 鉴权接口 GET /api/key/{contentId}:
   - 读请求 Cookie 里的 JWT → 验签 → 查会员/订阅是否有效
   - 通过 → 200 { key }(返回那把随机 key)
   - 不通过 → 401
④ 前端:先从 CDN 下密文 → fetch /api/key 拿 key → 浏览器本地 AES-GCM 解密 → 渲染
⑤ 支付闭环:Stripe/微信支付 webhook 里标记会员,之后 /api/key 才放行

鉴权那一段照抄 01-服务端硬墙 的 SKILL(JWT / HttpOnly Cookie / 支付回调 / 验签不查库),唯一区别:接口返回的是 key 而不是正文。这样正文带宽全甩给免费 CDN,你的服务器只保护一把钥匙。

#验收清单

  • 不输密码/无会员时,页面源码里能看到密文,但搜不到任何一句明文正文
  • 输对密码 / 拿到 key 后,内容正常还原(PDF/图片能看、文本能读)。
  • 错误密码被 GCM 直接拒绝(抛错,不出假内容)。
  • 加密→解密字节级一致(对比原文件 size 和内容)。
  • 变种 B:未登录请求 /api/key 返回 401,响应体不含 key;密文 CDN 链接公开可下但下到的是乱码。

#老实提醒(别自欺)

  1. 强度 = 密钥强度。变种 A 用短口令/纯数字有离线爆破风险;敏感内容用长随机密码或直接上变种 B 的随机 key。
  2. 密文被下走后可永久离线爆破,到点删除保护不了已抓走密文的人。
  3. 防不住合法者转发(拿到明文/密钥的人能再分发)——这是所有内容墙的通病。
  4. 元数据会露:文件名/大小常在页面里可见,别把敏感信息写进文件名。

#技术选型建议

  • 加密器:Node 内置 crypto(零依赖)或服务端任意语言;浏览器端 WebCrypto。
  • 托管:litterbox(临时)/ GitHub Pages / Cloudflare R2 / S3 / IPFS(长期)。
  • 变种 B 鉴权:复用 01 的 Supabase + JWT + HttpOnly Cookie。
  • 现成轮子:FileBeam 的 send_secure.mjs / lib/process.ts:buildPasswordGate() 直接抄。

来源:沉淀/内容付费墙方案/README.md 及其下 3 套 SKILL.md(01-服务端硬墙 / 02-前端软墙 / 03-客户端加密墙)(整理于 2026-08-18)