1. 先看整体:这个 Demo 到底在演示什么
它不是一个普通下载页,而是在演示“二维码背后的完整链路”。学生扫码看到的只是一个入口,真正复杂的是入口后面的服务端和浏览器脚本配合。
/yyq/share/jumpShare.html?userId=25888相当于真实二维码扫出来的 jumpShare.html。
服务端生成随机子域名,并按健康评分排序。
/32位token/8位token/index.html每次访问都不一样,用来模拟真实案例中的动态落地路径。
服务端出题,浏览器 JS 算题,服务端验题。通过后才能看到落地页。
下载链接有 HMAC 签名、过期时间、一次性 nonce 和审计记录。
小白记忆法:二维码只是门牌号,真正的系统在门后面。门后面有线路调度、环境校验、下载授权和审计记录。
2. 文件地图:每个文件负责什么
server.js整个 Demo 的后端大脑。负责路由、动态线路、Guard、下载签名、审计面板、静态文件服务。
public/index.html首页演示面板。展示二维码、下载配置、Guard 日志和教学说明。
public/guard.js静态 Guard 教学脚本。用于演示前端检测、告警、降级逻辑。
public/code-walkthrough.html你正在看的代码讲解页。给学生逐块解释代码。
homework/学生提交的 Node 原版作业。重点看页面协议、download-app.js、Guard、签名下载和远程验收脚本。
homework-java/老师改造后的 Java 21 企业级后端。页面保持学生原版,后端换成 Spring Boot、MySQL、Redis、审计、限流和生产配置闸门。
scripts/verify-teaching-flow.js自动验收脚本。一条 npm test 跑通关键链路,证明不是口头实现。
config.json安全演示配置。给开源仓库使用,不包含真实课堂服务器配置。
real-download-config.json本机/服务器可选真实配置文件,已被 .gitignore 排除,不提交到仓库。
qa-report/每次升级后的验证报告。学生质疑时,老师可以拿证据说话。
3. 请求怎么流转:从浏览器到 Node
浏览器每次访问一个 URL,Node 的 server.js 都会进入同一个入口:
const server = http.createServer((req, res) => {
const url = new URL(req.url, 'http://localhost:' + PORT);
const pathname = safeDecodeURIComponent(url.pathname);
...
});
这段代码的意思是:
| 代码 | 小白解释 |
|---|---|
req | 浏览器发来的请求,比如访问了哪个地址、带了什么 Cookie。 |
res | 服务器要返回给浏览器的响应,比如 HTML、JSON、302 跳转、403 拒绝。 |
url.pathname | URL 的路径部分,例如 /healthz、/download/signed。 |
if (pathname === ...) | 路由判断:不同路径进入不同功能。 |
主要路由如下:
| 路由 | 用途 |
|---|---|
/healthz | 健康检查,告诉我们服务是否运行、Node 版本、功能开关。 |
/audit.html | 安全审计面板,给老师投屏展示事件。 |
/audit/events | 审计数据接口,返回 JSON。 |
/yyq/share/jumpShare.html | 二维码入口页,生成线路和动态路径。 |
/_line/... | 模拟真实随机线路落地页,并触发 Guard 校验。 |
/_guard/auto.js | 动态 Guard 脚本,浏览器用它计算 challenge response。 |
/share/landingConfig.html | 下载配置接口,返回 Android、iOS、OpenInstall 和签名下载链接。 |
/download/signed | 签名下载入口,正确签名才 302,错误/过期/重放返回 403。 |
4. server.js 逐块拆解
4.1 引入模块
const crypto = require("crypto");
const http = require("http");
const fs = require("fs");
const path = require("path");
| 模块 | 作用 |
|---|---|
crypto | 生成随机数、SHA-256、HMAC 签名。安全链路都靠它。 |
http | 启动 Node HTTP 服务器。 |
fs | 读取配置文件和静态文件。 |
path | 处理文件路径,避免路径拼接出错。 |
4.2 配置读取
DEFAULT_CONFIG 是默认配置。readJsonConfig() 负责读取 JSON 文件,normalizeConfig() 负责把字段统一成字符串或数组。
const legacyConfig = readJsonConfig("config.json") || {};
const realConfig = readJsonConfig("real-download-config.json");
config = normalizeConfig(Object.assign({}, legacyConfig, realConfig || {}), ...);
意思是:先读仓库里的安全配置 config.json,如果部署服务器上有真实配置 real-download-config.json,就用真实配置覆盖。
为什么真实配置不提交?因为真实下载地址、真实渠道、真实来源属于课堂现场数据,迭代代码不应该夹带现场配置。
4.3 通用工具函数
| 函数 | 作用 | 小白解释 |
|---|---|---|
randomHex(len) | 生成随机十六进制字符串。 | 用来造随机子域名、token、nonce。 |
safeDecodeURIComponent() | 安全解码 URL/Cookie。 | 遇到坏编码也不让服务崩。 |
normalizeUserId() | 只保留数字 userId。 | 避免参数乱传造成链路不一致。 |
escapeHtml() | 转义 HTML 特殊字符。 | 防止把用户输入当成 HTML 执行。 |
4.4 线路健康评分
真实线路调度不是简单随机。Demo 里给三类父域设置基础权重和延迟:
const LINE_PROFILES = [
{ parentDomain: "hglive.org", weight: 96, baseLatencyMs: 420 },
{ parentDomain: "jtyw.org", weight: 86, baseLatencyMs: 640 },
{ parentDomain: "imagesworks.com.youfenfa.shop", weight: 78, baseLatencyMs: 780 }
];
| 函数 | 作用 |
|---|---|
getLineStats() | 拿到某个父域的统计数据,没有就初始化。 |
calculateLineScore() | 根据权重、成功率、延迟、失败次数算分。 |
generateLinePlan() | 生成随机子域名,并按分数排序。 |
recordLineSelected() | 线路被选中后,更新成功次数和平均延迟。 |
课堂一句话:随机子域名解决“每次入口不同”,健康评分解决“选哪条线路更合理”。
4.5 动态路径 token
function generatePathTokens(userId) {
const campaignToken = randomHex(32);
const landingToken = randomHex(8);
return {
landingPath: "/" + campaignToken + "/" + landingToken + "/index.html"
};
}
它模拟真实案例中的动态路径:
/53fe1a6f75cbee41564222f9e81f59de/825517ba/index.html?userId=25888
每次访问都变,学生看到“二维码固定但后面路径不固定”,原因就在这里。
升级后,路径 token 不只是展示。入口页生成 token 时,后台会把它登记到 pathTokenStore,并绑定:
| 绑定项 | 作用 |
|---|---|
campaignToken + landingToken | 证明这个路径确实由后台签发过。 |
userId | 防止把 A 用户的路径拿给 B 用户用。 |
lineHost | 防止把某条线路的 token 搬到另一条线路上。 |
exp | 过期时间,超过窗口就拒绝。 |
used | 是否已经落地使用过,用来防重放。 |
所以现在后台处理动态路径分两步:
第一步:用正则解析 /_line/32位token/8位token/index.html
第二步:去 pathTokenStore 校验它是不是后台签发、是否过期、是否重放、userId/lineHost 是否匹配
随便编一个路径会返回 403 unknown_path_token;同一个路径成功落地后再次访问会返回 403 replayed_path_token。
4.6 Guard Challenge
| 函数 | 作用 |
|---|---|
createGuardChallenge(req) | 服务端生成题目:cid、nonce、过期时间。 |
getDynamicGuardScript() | 动态生成浏览器 JS,让浏览器计算答案。 |
validateGuard(req) | 服务端检查浏览器提交的答案对不对。 |
getGuardForbiddenHtml() | 答案错时返回 403 页面。 |
小白理解:
服务端:我给你一道题
浏览器:我用 JS 算答案
服务端:答案正确,放行;答案错误,403
5. Guard.js 拆解:为什么不是“前端摆设”
项目里有两种 Guard:
| 文件/路由 | 用途 |
|---|---|
public/guard.js | 首页教学用静态脚本,展示前端检测和日志。 |
/_guard/auto.js?cid=... | 服务端动态生成的挑战脚本,参与真实 Guard 放行。 |
动态 Guard 脚本的核心动作:
1. 读取 guard cookie
2. 拿 cid、nonce、exp
3. 用浏览器 Web Crypto 算 SHA-256
4. 写入 guardret 和 guard_pass cookie
5. 跳回原页面
重点:如果只有前端 JS,学生可以关掉或改掉;但这里服务端还会验 Cookie,所以它不是简单摆设。
6. 下载与 token:为什么错误链接会 403
下载配置接口 /share/landingConfig.html 会返回普通配置,也会返回签名下载地址:
{
"androidDownload": "...",
"signedAndroidDownload": "/download/signed?token=...&sig=...",
"signedDownloadTtlSeconds": 300
}
签名 token 的组成
| 字段 | 意思 |
|---|---|
v | token 版本,方便以后升级格式。 |
kid | 密钥编号,用于密钥轮换。 |
platform | android 或 ios。 |
userId | 分享人或用户参数。 |
targetUrl | 真正下载目标。 |
exp | 过期时间。 |
nonce | 一次性随机数,防重放。 |
sig | HMAC 签名,证明 token 是服务端签的。 |
验证顺序
bad_token。bad_platform 或 bad_target。expired_token。kid 找密钥,重新计算 HMAC,不一致返回 bad_signature。target_not_allowed。replayed_token。302 Location: 真实下载地址。7. 审计和线路评分:老师如何拿证据说话
项目里有一个内存审计队列 auditEvents,关键动作都会记录:
entry_line_plan入口生成线路计划。
guard_challenge服务端下发 Guard 题目。
guard_passedGuard 验证通过。
guard_rejectedGuard 验证失败。
download_redirect签名下载通过并 302。
download_rejected签名下载失败,比如重放。
打开 /audit.html 可以看到这些事件。它的价值是:学生问“真的实现了吗”,老师可以让学生自己看审计记录。
线路健康评分也在这里展示,包含 score、generated、selected、success、avgLatencyMs。
8. 测试脚本:一条命令证明核心功能
scripts/verify-teaching-flow.js 是老师的“验收老师”。它会自己启动临时服务,然后自动访问接口。
npm test
它验证这些事:
| 检查项 | 为什么重要 |
|---|---|
/healthz 返回 200 | 证明服务能启动。 |
| 签名下载第一次 302 | 证明正确 token 可以放行。 |
| 同一下载链接第二次 403 | 证明一次性下载 token 防重放生效。 |
| 随便编动态路径 403 | 证明路径 token 不是只解析,还要后台登记。 |
| 同一动态路径第二次 403 | 证明路径 token 也支持一次性防重放。 |
| 坏签名 403 | 证明不能随便改 token。 |
| 过期 token 403 | 证明 token 有时间边界。 |
| 旧密钥仍可验证 | 证明密钥轮换窗口可用。 |
| Guard 正确通过,错误拒绝 | 证明 Guard 不是静态页面。 |
| 审计接口有事件 | 证明关键动作有记录。 |
9. 配置文件:为什么有 config.json 和 real-download-config.json
| 文件 | 提交到 Git 吗 | 用途 |
|---|---|---|
config.json | 提交 | 安全演示配置,地址用 example.com,方便学生拉代码。 |
real-download-config.json | 不提交 | 课堂服务器真实配置,保留在部署机或老师本机。 |
这种做法叫“代码和环境配置分离”。代码可以公开迭代,真实环境值留在服务器。
小白不要把数据库密码、SSH 密钥、证书私钥、真实 API Key 提交到 Git。
10. 课堂讲法:怎么让学生听懂
先讲一句话
这个 Demo 不是为了把跳转做得神秘,而是为了把二维码背后的工程链路讲透明。
再讲三层
扫码后页面会生成随机线路和动态路径。
Node 根据路由生成 HTML、JS、Cookie、token 和审计事件。
健康评分、防重放、密钥轮换、审计面板、自动验收。
只演示合规防盗链、反爬、归因和微信引导,不演示绕风控。
最后给学生的总结
二维码只是入口。
真正专业的是入口后面的系统:
能动态调度、能校验访问、能保护下载、能记录审计、能自动验收。
11. 生产化演进:从单机教学到分布式系统
现在 Demo 又补了一层生产化底座,但老师要诚实说明:这叫“朝生产靠近”,不是一夜之间变成完整生产平台。
运行时状态会保存到 data/runtime-state.json,服务重启后可以恢复审计、路径 token、下载 nonce 和线路统计。
/monitor/status 返回结构化状态和 alerts,适合脚本或监控系统读取。
/metrics 输出标准文本指标,可以接 Prometheus 和 Grafana。
设置 AUDIT_DASHBOARD_TOKEN 后,审计页面和事件接口需要 Bearer Token。
npm run probe:lines 可以对配置的线路做 HTTP HEAD 探测。
deploy/ 里有 Nginx wildcard、systemd 和 CI/CD 示例。
下一步真正分布式要把文件持久化替换成 Redis:路径 token 用 Redis TTL,下载 nonce 用 SETNX,审计日志进 Redis Stream、Kafka 或 ClickHouse。
12. 学生 Node 原版:哪里做得对,哪里被老师补强
学生原版不是简单页面,它已经具备一个完整下载链路的雏形。老师验收时不能只看“页面能不能打开”,还要看前端和后端有没有约定、状态有没有校验、别人能不能复现。
12.1 学生原版页面协议
学生原版前端 homework/src/download-app.js 会请求:
GET /share/landingConfig.html
然后按这个格式读取后端配置:
{
"m_istatus": 1,
"m_strMessage": "success",
"m_object": {
"shareUserId": "25888",
"channelCode": "classroom",
"androidDownload": "/download/signed?token=...&sig=...",
"iosDownload": "/download/signed?token=...&sig=...",
"androidDownloadMirrors": []
}
}
| 字段 | 给学生的解释 |
|---|---|
m_istatus | 接口状态。等于 1 才说明配置可用,否则按钮会禁用。 |
m_object | 真正业务数据。下载地址、渠道、分享人都在这里。 |
androidDownload | Android 最终下载地址。老师改造后这里不是裸地址,而是 Java 签名下载入口。 |
iosDownload | iOS 最终下载地址。真实业务里可指向 App Store、Universal Link 或合规下载页。 |
androidDownloadMirrors | Android 镜像线路。生产可根据健康状态选择镜像。 |
这就是“页面不变,后端迁移”的关键:前端继续读 m_istatus/m_object,后端从 Node 换成 Java。
12.2 学生作业的优点
下载页不把地址写死在 HTML 里,而是通过配置接口下发。
不是只做按钮跳转,而是考虑了调试、环境检测和事件上报。
入口页和落地页分离,已经接近真实二维码链路。
Node 版本适合讲原理,Java 版本适合讲企业后端分层。
12.3 老师补强点
| 学生原版 | 老师补强 | 为什么要补 |
|---|---|---|
| 本地可跑 | 远程 HTTPS、Nginx、systemd 部署 | 让其他同学也能联调,不只老师本机能跑。 |
| 动态路径存在 | 后台登记、过期、绑定、重放拒绝 | 证明 token 不是摆设。 |
| 下载地址返回 | HMAC 签名下载、一次性 nonce | 防盗链、防篡改、防重放。 |
| 有事件上报 | MySQL 审计表、metrics、远程 smoke | 课堂上可以拿证据说话。 |
| Node 单工程 | Java 分层工程 | 便于讲 Controller、Service、Repository、配置闸门。 |
13. Java 21 企业级工程:老师改造版怎么讲
homework-java/ 不是把 Node 代码逐行翻译成 Java,而是把同一条二维码下载链路拆成企业后端常见分层。页面仍然是学生原版“蝶衣官方下载”,服务端能力由 Java 接管。
13.1 Java 工程目录地图
| 目录/文件 | 职责 | 课堂讲法 |
|---|---|---|
controller/EntryController.java | 入口、动态路径、Guard 页面、配置接口、签名下载 | 所有浏览器请求先到 Controller,再分发给 Service。 |
service/LineService.java | 生成随机线路和动态路径计划 | 相当于“调度员”,决定本次访问走哪条线路。 |
service/PathTokenService.java | 动态路径 token 登记、校验、使用 | 证明路径是后台签发的,不是随便编的。 |
service/GuardService.java | 服务端 challenge、前端 response、服务端验签 | 服务端出题,浏览器算题,服务端验题。 |
service/DownloadTokenService.java | 签名下载 token、过期、nonce 防重放 | 下载地址不裸奔,必须带服务端签名。 |
service/LandingSessionService.java | pageToken 会话 | 配置接口不是谁都能调,必须从真实落地页进来。 |
repository/ | MySQL 持久化访问 | 审计和路径 token 可以落库,不怕服务重启丢状态。 |
store/RedisShortStateStore.java | Redis 短状态共享 | 多实例部署时,Guard、pageToken、nonce 不再只存在单机内存。 |
config/ProductionStartupValidator.java | 生产配置闸门 | 弱密钥、示例域名、未启用 Redis 等危险配置直接拒绝启动。 |
13.2 Java 版真实链路
/yyq/share/jumpShare.html?userId=25888Java 生成动态线路和动态路径。
/_line/{campaignToken}/{landingToken}/index.html后台验证 token 已签发、未过期、未重放。
/_guard/auto.js浏览器计算 cookie response,服务端校验。
标题、按钮、JS 仍然是学生原版页面结构。
/share/landingConfig.html返回
m_istatus/m_object,兼容学生原版 JS。/download/signed正确签名 302,错误、过期、重放 403。
13.3 页面一致性怎么证明
老师改造后不是打开一个“老师自制页面”,而是让 Java 返回学生原版页面需要的资源和字段:
| 检查项 | 当前结果 |
|---|---|
| 页面标题 | 蝶衣官方下载 |
| 下载按钮 | id="downloadBtn" |
| 学生脚本 | /js/download-app.js |
| 配置字段 | m_istatus、m_object.androidDownload、m_object.iosDownload |
| 旧老师页标记 | 不再出现 青柠下载 |
13.4 Java 版怎么验收
cd homework-java
JAVA_HOME=/Users/zm/Downloads/codex-jdk/zulu21 PATH=/Users/zm/Downloads/codex-jdk/zulu21/bin:$PATH mvn test
BASE_URL=https://wi2w.dmbdz.com AUDIT_TOKEN=<服务器审计令牌> npm run test:remote
测试覆盖:未知动态路径拒绝、Guard challenge、Guard 放行、路径重放拒绝、pageToken 保护、签名下载首次 302、下载重放 403、metrics 鉴权、课堂讲解页可访问。
13.5 给学生的结论
学生 Node 原版解决“能跑和能演示”。
老师 Java 改造解决“能部署、能审计、能防重放、能多实例共享、能生产配置兜底”。
专业不是把页面改得不一样,而是在页面不变的前提下,把后端工程能力补上。
14. 参考二维码增强链路:多级中转、归因 SDK、Scheme
后面这个参考二维码不是单纯“二维码入口到下载页”,它更像三段式链路:入口页先执行一段 JS,再跳第二个中转页,最后到真正落地页。现在 Java 主扫码入口也已接入两级中转:/yyq/share/jumpShare.html 会先进入 /_relay/.../r1.html,再进入 /_relay/.../r2.html,最后回到原来的 /_line/.../index.html、Guard 和签名下载链路。
14.1 老师补的三块
/relay/start.html 到 /relay/hop/2/index.html 再到 /relay/final/index.html,模拟参考二维码的多跳结构。
/yyq/share/jumpShare.html 现在会先走 /_relay/{token}/r1.html 和 /_relay/{token}/r2.html,再进入原动态路径。
/attribution/config.html 同时返回 shareTraceLike 和 openInstallLike,说明第三方 SDK 在链路中的位置。
/scheme-demo.html 展示 mttbrowser://url=...,但不自动执行,让学生理解“能做”和“该不该做”的边界。
最终下载仍然走 /download/signed,所以错误、过期、重放依然会 403。
14.2 和参考二维码怎么对应
| 参考二维码现象 | 课堂代码对应 | 老师讲法 |
|---|---|---|
| 入口页加载后通过 JS 跳下一页 | /relay/start.html | 页面不是终点,只是负责把参数带到下一跳。 |
| 第二跳继续跳最终落地页 | /relay/hop/2/index.html | 多跳可以用于线路切换、灰度、归因参数整理。 |
| 第一跳加载腾讯云 COS 图片 | https://yuyu-1302030497.cos.ap-shanghai.myqcloud.com/1800/002.jpg | 当前探测为 200,课堂页直接加载。 |
| 第二跳加载新浪图片 | https://ww1.sinaimg.cn/large/001zzwxlgy1ho1l832608j30u01hcacb.jpg | 当前会 302 到新浪默认图,仍保留原 URL 便于和样例对照。 |
| 最终页加载 ShareTrace SDK | /attribution/config.html 的 shareTraceLike | 课堂版不加载真实 SDK,只模拟字段和调用顺序。 |
| 微信/QQ 环境可能尝试外部浏览器 scheme | /scheme-demo.html | 只展示、复制和解释,不做强制唤起。 |
14.3 小白版流程
/relay/start.html?userId=2521。location.href,进入第二跳。userId/channel 到最终落地页。/attribution/pre-download.html。/attribution/config.html,服务端返回归因适配信息和签名下载地址。这就能严谨回答学生:我们没有照抄第三方混淆脚本,而是把它背后的工程结构拆出来,并补上透明归因、pageToken 保护、签名下载和审计记录。比原案例更适合教学,也更合规。