二维码链路教学 Demo 代码拆解

这份页面给零基础同学看:不要求先懂 Node,也不要求先懂 Java。我们从“浏览器访问一个网址”开始,把学生原版 Node、老师改造 Java、每个核心函数和每条请求链路讲清楚。

1. 先看整体:这个 Demo 到底在演示什么

它不是一个普通下载页,而是在演示“二维码背后的完整链路”。学生扫码看到的只是一个入口,真正复杂的是入口后面的服务端和浏览器脚本配合。

1
二维码入口
/yyq/share/jumpShare.html?userId=25888
相当于真实二维码扫出来的 jumpShare.html。
2
生成动态线路
服务端生成随机子域名,并按健康评分排序。
3
生成动态路径 token
/32位token/8位token/index.html
每次访问都不一样,用来模拟真实案例中的动态落地路径。
4
Guard Challenge
服务端出题,浏览器 JS 算题,服务端验题。通过后才能看到落地页。
5
签名下载与审计
下载链接有 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.pathnameURL 的路径部分,例如 /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)服务端生成题目:cidnonce、过期时间。
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 的组成

字段意思
vtoken 版本,方便以后升级格式。
kid密钥编号,用于密钥轮换。
platformandroid 或 ios。
userId分享人或用户参数。
targetUrl真正下载目标。
exp过期时间。
nonce一次性随机数,防重放。
sigHMAC 签名,证明 token 是服务端签的。

验证顺序

1
解析 token,解析失败返回 bad_token
2
检查平台和目标地址,不合法返回 bad_platformbad_target
3
检查是否过期,过期返回 expired_token
4
根据 kid 找密钥,重新计算 HMAC,不一致返回 bad_signature
5
检查目标是否在白名单,不在返回 target_not_allowed
6
检查 nonce 是否用过,用过返回 replayed_token
7
全部通过,记录 nonce,返回 302 Location: 真实下载地址

7. 审计和线路评分:老师如何拿证据说话

项目里有一个内存审计队列 auditEvents,关键动作都会记录:

entry_line_plan

入口生成线路计划。

guard_challenge

服务端下发 Guard 题目。

guard_passed

Guard 验证通过。

guard_rejected

Guard 验证失败。

download_redirect

签名下载通过并 302。

download_rejected

签名下载失败,比如重放。

打开 /audit.html 可以看到这些事件。它的价值是:学生问“真的实现了吗”,老师可以让学生自己看审计记录。

线路健康评分也在这里展示,包含 scoregeneratedselectedsuccessavgLatencyMs

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,适合脚本或监控系统读取。

Prometheus 指标

/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真正业务数据。下载地址、渠道、分享人都在这里。
androidDownloadAndroid 最终下载地址。老师改造后这里不是裸地址,而是 Java 签名下载入口。
iosDownloadiOS 最终下载地址。真实业务里可指向 App Store、Universal Link 或合规下载页。
androidDownloadMirrorsAndroid 镜像线路。生产可根据健康状态选择镜像。

这就是“页面不变,后端迁移”的关键:前端继续读 m_istatus/m_object,后端从 Node 换成 Java。

12.2 学生作业的优点

前端协议清晰

下载页不把地址写死在 HTML 里,而是通过配置接口下发。

有 Guard 意识

不是只做按钮跳转,而是考虑了调试、环境检测和事件上报。

有动态入口

入口页和落地页分离,已经接近真实二维码链路。

可继续工程化

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.javapageToken 会话配置接口不是谁都能调,必须从真实落地页进来。
repository/MySQL 持久化访问审计和路径 token 可以落库,不怕服务重启丢状态。
store/RedisShortStateStore.javaRedis 短状态共享多实例部署时,Guard、pageToken、nonce 不再只存在单机内存。
config/ProductionStartupValidator.java生产配置闸门弱密钥、示例域名、未启用 Redis 等危险配置直接拒绝启动。

13.2 Java 版真实链路

1
入口
/yyq/share/jumpShare.html?userId=25888
Java 生成动态线路和动态路径。
2
动态路径
/_line/{campaignToken}/{landingToken}/index.html
后台验证 token 已签发、未过期、未重放。
3
Guard
/_guard/auto.js
浏览器计算 cookie response,服务端校验。
4
学生原版页面
标题、按钮、JS 仍然是学生原版页面结构。
5
配置接口
/share/landingConfig.html
返回 m_istatus/m_object,兼容学生原版 JS。
6
签名下载
/download/signed
正确签名 302,错误、过期、重放 403。

13.3 页面一致性怎么证明

老师改造后不是打开一个“老师自制页面”,而是让 Java 返回学生原版页面需要的资源和字段:

检查项当前结果
页面标题蝶衣官方下载
下载按钮id="downloadBtn"
学生脚本/js/download-app.js
配置字段m_istatusm_object.androidDownloadm_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,再进入原动态路径。

归因 SDK 抽象层

/attribution/config.html 同时返回 shareTraceLikeopenInstallLike,说明第三方 SDK 在链路中的位置。

Scheme 合规演示

/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.htmlshareTraceLike课堂版不加载真实 SDK,只模拟字段和调用顺序。
微信/QQ 环境可能尝试外部浏览器 scheme/scheme-demo.html只展示、复制和解释,不做强制唤起。

14.3 小白版流程

1
扫码打开 /relay/start.html?userId=2521
2
入口页 JS 执行 location.href,进入第二跳。
3
第二跳继续带着 userId/channel 到最终落地页。
4
最终页点击“模拟下载归因”,先上报 /attribution/pre-download.html
5
再请求 /attribution/config.html,服务端返回归因适配信息和签名下载地址。

这就能严谨回答学生:我们没有照抄第三方混淆脚本,而是把它背后的工程结构拆出来,并补上透明归因、pageToken 保护、签名下载和审计记录。比原案例更适合教学,也更合规。