From 417a16e055a5d5a9e24fcaa25d5e24ae109b7328 Mon Sep 17 00:00:00 2001 From: hzj <1304805162@qq.com> Date: Tue, 21 Apr 2026 16:42:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8A=A0=E5=9B=BA=E5=89=8D=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 2 +- .../atu-prod-hardening-playbook.zh-CN.md | 361 ++++++++++++++++ docs/security/frontend-hardening.md | 295 ++++++++++++++ docs/security/frontend-hardening.zh-CN.md | 324 +++++++++++++++ .../frontend-hardening.zh-CN.updates.md | 58 +++ package.json | 5 +- scripts/build-with-version.js | 36 ++ scripts/sync-security-docs.js | 72 ++++ src/App.vue | 73 +++- src/components/BackgroundLayer.vue | 161 +++++++- src/config/imagePaths.js | 20 +- src/config/security.js | 54 +++ src/directives/smartImage.js | 73 ++-- src/main.js | 5 + src/utils/http.js | 14 + src/utils/image/imageCacheManager.js | 11 +- src/utils/protectedAssetRuntime.js | 87 ++++ src/utils/protectedAssets.js | 66 +++ src/utils/routeGuard.js | 354 ++++------------ src/utils/runtimeSecurity.js | 130 ++++++ src/views/Activities/LesserBairam/index.vue | 7 +- src/views/Activities/LoginReward/index.vue | 12 +- .../Activities/LuckyDollars/Season3/index.vue | 7 +- .../Activities/LuckyDollars/Season4/index.vue | 60 +-- .../Activities/SpringFestival/Ranking.vue | 326 +++++---------- src/views/Activities/SpringFestival/Task.vue | 3 +- src/views/Activities/SpringFestival/index.vue | 22 +- src/views/Ranking/Couple/Ranking.vue | 229 ++++------- src/views/Ranking/Couple/index.vue | 16 +- src/views/Ranking/GamesKing/index.vue | 2 +- src/views/Ranking/KingAndQueen/TopList.vue | 9 +- src/views/Ranking/Overall/Ranking.vue | 69 ++-- src/views/Ranking/WeeklyStar/WeeklyStar.vue | 22 +- vite.config.js | 384 +++++++++++++++--- 34 files changed, 2478 insertions(+), 891 deletions(-) create mode 100644 docs/security/atu-prod-hardening-playbook.zh-CN.md create mode 100644 docs/security/frontend-hardening.md create mode 100644 docs/security/frontend-hardening.zh-CN.md create mode 100644 docs/security/frontend-hardening.zh-CN.updates.md create mode 100644 scripts/build-with-version.js create mode 100644 scripts/sync-security-docs.js create mode 100644 src/config/security.js create mode 100644 src/utils/protectedAssetRuntime.js create mode 100644 src/utils/protectedAssets.js create mode 100644 src/utils/runtimeSecurity.js diff --git a/.env.development b/.env.development index 309ce5c..b199985 100644 --- a/.env.development +++ b/.env.development @@ -18,7 +18,7 @@ VITE_APP_TITLE=Azizi H5 (Development) VITE_API_BASE_URL=https://api.likeichat.com # 正式用户 8832199 -VITE_USER_AUTH=9125B94C72CAA512039AD0F85348F56C.djIlM0ExOTkwNjg1NTY1MzMzNTkwMDE3JTNBTElLRUklM0ExNzc2NTEzNDAyMjM4JTNBMTc3MzkyMTQwMjIzOA== +VITE_USER_AUTH=8C8D1E758D7B032FFB5664C4FE41C91C.djIlM0ExOTkwNjg1NTY1MzMzNTkwMDE3JTNBTElLRUklM0ExNzc5MjUxMDM2MjEyJTNBMTc3NjY1OTAzNjIxMg== # 正式用户 8826796 # VITE_USER_AUTH=23116F749A21862A2B7217CDEE0FBF45.djIlM0ExOTYzNzkwMDM3NzQwNDY2MTc4JTNBTElLRUklM0ExNzc4OTAxNTg4MDA5JTNBMTc3NjMwOTU4ODAwOQ== # 正式用户 10086 diff --git a/docs/security/atu-prod-hardening-playbook.zh-CN.md b/docs/security/atu-prod-hardening-playbook.zh-CN.md new file mode 100644 index 0000000..e7dcdd6 --- /dev/null +++ b/docs/security/atu-prod-hardening-playbook.zh-CN.md @@ -0,0 +1,361 @@ +# `atu-prod` 分支前端加固迁移执行手册 + +## 目的 + +这份文档用于在后续切换到 `atu-prod` 分支后,按固定步骤复用当前分支已经落地的前端防扒取加固方案。 + +使用方式很简单: + +- 当你切到 `atu-prod` 后,只要说明“按 `atu-prod-hardening-playbook.zh-CN.md` 执行” +- 我就按这份文档中的顺序检查、迁移、修复和验证 + +这份文档不是泛泛的说明,而是面向实际执行的作业手册。 + +## 适用前提 + +- `atu-prod` 分支与当前分支在业务功能上大体一致 +- 主要差异集中在样式、布局、局部结构命名 +- 活动页和榜单页的核心交互、接口、资源使用方式基本相同 + +如果后续发现 `atu-prod` 在页面结构上差异很大,这份手册仍然能作为基线,但执行时要允许局部调整。 + +## 迁移目标 + +切换到 `atu-prod` 后,需要尽量恢复当前分支已经完成的这些能力: + +1. 受保护页面仅允许在 App 环境下访问 +2. 运行时基础防调试、防复制、防拖拽和动态水印 +3. `Activities / Ranking` 图片地址保护与运行时解析 +4. `background-image` 相关真实图链泄露点收口 +5. 构建产物哈希化和可读性降低 +6. 生产相关模式日志清理与保留日志开关 +7. `v-show` 到 `v-if` 的主内容延迟挂载改造 +8. `BackgroundLayer` 的 `useCanvas` 背景合成方案 +9. 明显语义类名收口 +10. 已发现问题页的修复和清理 + +## 执行顺序 + +后续在 `atu-prod` 分支执行时,按下面顺序做,不要跳步。 + +### 第 1 步:先对照基础能力文件 + +优先检查这些全局能力文件是否已经存在、是否需要同步: + +- [src/utils/routeGuard.js](/D:/programs/likei-h5/src/utils/routeGuard.js) +- [src/utils/http.js](/D:/programs/likei-h5/src/utils/http.js) +- [src/config/security.js](/D:/programs/likei-h5/src/config/security.js) +- [src/utils/runtimeSecurity.js](/D:/programs/likei-h5/src/utils/runtimeSecurity.js) +- [src/main.js](/D:/programs/likei-h5/src/main.js) +- [src/App.vue](/D:/programs/likei-h5/src/App.vue) +- [src/config/imagePaths.js](/D:/programs/likei-h5/src/config/imagePaths.js) +- [src/utils/protectedAssets.js](/D:/programs/likei-h5/src/utils/protectedAssets.js) +- [src/utils/protectedAssetRuntime.js](/D:/programs/likei-h5/src/utils/protectedAssetRuntime.js) +- [src/directives/smartImage.js](/D:/programs/likei-h5/src/directives/smartImage.js) +- [src/utils/image/imageCacheManager.js](/D:/programs/likei-h5/src/utils/image/imageCacheManager.js) +- [src/components/BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue) +- [vite.config.js](/D:/programs/likei-h5/vite.config.js) +- [package.json](/D:/programs/likei-h5/package.json) +- [scripts/build-with-version.js](/D:/programs/likei-h5/scripts/build-with-version.js) + +这一步的目标是先把“全局基础设施”对齐,再进页面级迁移。 + +### 第 2 步:扫描 `Activities / Ranking` 页面 + +重点扫描这两个目录: + +- [src/views/Activities](/D:/programs/likei-h5/src/views/Activities) +- [src/views/Ranking](/D:/programs/likei-h5/src/views/Ranking) + +执行时重点看四类点: + +1. 是否仍有大量直接 `` +2. 是否仍有 `background-image: url(...)` +3. 是否存在 `v-show="!isLoading"` 控制主内容 +4. 是否存在明显语义类名、明显业务含义 chunk 入口、测试日志 + +### 第 3 步:优先迁移高风险页面 + +后续在 `atu-prod` 中优先处理这些已经确认过风险较高、且当前分支已加固的页面: + +- [src/views/Activities/LesserBairam/index.vue](/D:/programs/likei-h5/src/views/Activities/LesserBairam/index.vue) +- [src/views/Activities/LoginReward/index.vue](/D:/programs/likei-h5/src/views/Activities/LoginReward/index.vue) +- [src/views/Activities/LuckyDollars/Season3/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season3/index.vue) +- [src/views/Activities/LuckyDollars/Season4/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season4/index.vue) +- [src/views/Activities/SpringFestival/index.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/index.vue) +- [src/views/Activities/SpringFestival/Ranking.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/Ranking.vue) +- [src/views/Activities/SpringFestival/Task.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/Task.vue) +- [src/views/Ranking/Couple/index.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/index.vue) +- [src/views/Ranking/Couple/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/Ranking.vue) +- [src/views/Ranking/GamesKing/index.vue](/D:/programs/likei-h5/src/views/Ranking/GamesKing/index.vue) +- [src/views/Ranking/KingAndQueen/TopList.vue](/D:/programs/likei-h5/src/views/Ranking/KingAndQueen/TopList.vue) +- [src/views/Ranking/Overall/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Overall/Ranking.vue) +- [src/views/Ranking/WeeklyStar/WeeklyStar.vue](/D:/programs/likei-h5/src/views/Ranking/WeeklyStar/WeeklyStar.vue) + +## 页面级迁移检查清单 + +后续每处理一个页面,都按这份清单检查。 + +### A. 图片链路 + +- 是否统一通过 `getPngUrl()` / `getWebpUrl()` 走受保护地址 +- 是否尽量使用 `v-smart-img` +- 是否存在直接暴露真实 OSS 地址的模板位点 +- 是否存在必须保留的 CSS 背景图场景 + +### B. 背景层 + +- 页面背景是否可改成 [BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue) +- 如果是多层纯装饰背景,是否开启 `:useCanvas="true"` +- 如果只是普通内容图,不强制改成 canvas + +说明: + +- `useCanvas` 不应一刀切设成默认值 `true` +- 它只针对敏感背景层启用,避免把普通页面也全部切到更重的渲染路径 + +### C. 主内容挂载时机 + +- 页面是否仍使用 `v-show="!isLoading"` 控制主内容 +- 如果页面存在预加载和监听时序问题,优先改成 `v-if="!isLoading"` +- 如果页面依赖 `IntersectionObserver`,应在 `await nextTick()` 后再注册 +- 如果预加载可能失败,确保在 `finally` 中放行主内容,避免整页空白 + +### D. 语义暴露 + +- 是否有过于直白的局部类名 +- 是否有业务含义明显的本地变量名或测试痕迹 +- 是否有无用日志、调试文案、历史残留注释 + +### E. 组件生命周期 + +- `IntersectionObserver` 是否在卸载时 `disconnect()` +- 定时器是否在卸载时清理 +- 残留监听代码是否已经失效却未移除 + +## 已知问题与处理记录 + +后续迁到 `atu-prod` 时,优先留意这些已经踩过的坑。 + +### 1. `v-show` 会让主内容 DOM 提前暴露 + +现象: + +- 页面还在预加载时,DOM 已经被挂载 +- 容易被直接从 `Elements` 面板里抄结构 +- 某些触底监听也会因为节点时序问题失灵 + +处理方式: + +- 将主内容改为 `v-if="!isLoading"` +- 对依赖监听的组件,在 `nextTick()` 后注册观察器 +- 在 `finally` 中放行主内容 + +涉及页面: + +- `LoginReward` +- `LuckyDollars/Season4` +- `SpringFestival` +- `Ranking/Couple` +- `Ranking/Overall` + +### 2. `IntersectionObserver` 挂载时机不稳 + +现象: + +- 监听节点存在,但绑定太早 +- 页面结构还没稳定时就执行 `observe()` +- 页面切走后观察器残留 + +处理方式: + +- `await nextTick()` +- 再 `observer.observe(...)` +- `onUnmounted` 时 `disconnect()` + +重点页面: + +- [src/views/Activities/SpringFestival/Ranking.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/Ranking.vue) + +### 3. `Ranking/Couple/Ranking.vue` 曾有残留无效加载监听代码 + +现象: + +- 组件里保留了已经取消的加载模块逻辑 +- 相关 `ref` 节点已不存在,但监听代码还在 + +处理方式: + +- 清理失效的 `IntersectionObserver` 逻辑 +- 删除无效加载模块相关变量 +- 保留当前真实在用的数据展示逻辑 + +### 4. `Ranking/Couple/Ranking.vue` 曾出现乱码注释 + +现象: + +- 早期编辑和编码处理后,中文注释出现乱码 + +处理方式: + +- 清理乱码注释 +- 统一改为中文注释 +- 保持逻辑不变,只修复可读性 + +### 5. `BackgroundLayer` 为什么不是默认 `useCanvas=true` + +结论: + +- 因为它是公共组件 +- 默认全开会把所有背景层都切到 canvas 路径 +- 会增加普通页面的图片加载、重绘和兼容风险 + +当前策略: + +- 只在高风险的 `Activities / Ranking` 页面显式传入 `:useCanvas="true"` + +### 6. `toCssBackgroundImage()` 的作用 + +结论: + +- 它不是加密层 +- 它是把 `likei-protected:` 地址还原成 CSS 可用的 `url(...)` +- 主要用于必须保留 `background-image` 的点位 + +相关文件: + +- [src/utils/protectedAssets.js](/D:/programs/likei-h5/src/utils/protectedAssets.js) + +## 构建与验证步骤 + +### 基础构建 + +执行: + +```bash +npm run build +``` + +预期: + +- 构建成功 +- 保留已知提示,但不新增本轮改动引起的报错 + +### 生产态验证 + +执行: + +```bash +npm run dev:prod +``` + +或: + +```bash +npm run build +npm run preview +``` + +验证点: + +- 非 App 环境访问受保护页会被拦截 +- 公共页可正常打开 +- `Activities / Ranking` 页面的图片仍能显示 +- `BackgroundLayer` 的 canvas 背景仍能正常渲染 +- 触底加载和倒计时逻辑不受影响 + +### 日志构建验证 + +执行: + +```bash +npm run build:prod +npm run build:prod:logs +npm run build:atu-prod +npm run build:atu-prod:logs +``` + +验证点: + +- 默认生产相关包清理 `console.log`、`console.debug`、`console.info`、`debugger` +- 带 `:logs` 的命令仍保留调试日志,便于 App 内定位问题 +- Jenkins 使用原有命令不需要改发布流程 + +## 建议的迁移方法 + +后续切到 `atu-prod` 后,建议按下面方式推进: + +1. 先同步全局基础能力文件 +2. 再扫描 `Activities / Ranking` +3. 先迁移高风险页面 +4. 每迁移一页就做局部验证 +5. 最后统一跑 `npm run build` +6. 再补文档同步 + +## 后续文档同步方法 + +如果后续主分支继续更新了这些安全文档,想把最新 md 同步到当前分支,不需要再手工复制。 + +先决条件: + +- 主分支上的文档更新已经提交到 Git +- 当前已经切换到目标分支,例如 `atu_prod` + +执行命令: + +```bash +npm run docs:sync:security -- master +``` + +如果你想从远端主分支同步,也可以用: + +```bash +npm run docs:sync:security -- origin/master +``` + +这条命令会同步这些文件: + +- `docs/security/frontend-hardening.md` +- `docs/security/frontend-hardening.zh-CN.md` +- `docs/security/frontend-hardening.zh-CN.updates.md` +- `docs/security/atu-prod-hardening-playbook.zh-CN.md` + +对应脚本文件: + +- [scripts/sync-security-docs.js](/D:/programs/likei-h5/scripts/sync-security-docs.js) + +注意: + +- 如果源分支里还没有提交这些 md,脚本会直接报错 +- 所以推荐流程是“先在主分支提交文档,再切到目标分支执行同步命令” + +## 文档同步要求 + +后续在 `atu-prod` 做迁移时,文档也要同步更新,至少包含: + +- 本次迁移到 `atu-prod` 的实际范围 +- 哪些页面已迁移 +- 哪些页面因结构差异暂未迁移 +- 新遇到的问题和处理方式 +- 最终验证结果 + +建议优先更新这些文档: + +- [docs/security/frontend-hardening.zh-CN.md](/D:/programs/likei-h5/docs/security/frontend-hardening.zh-CN.md) +- [docs/security/frontend-hardening.zh-CN.updates.md](/D:/programs/likei-h5/docs/security/frontend-hardening.zh-CN.updates.md) +- [docs/security/atu-prod-hardening-playbook.zh-CN.md](/D:/programs/likei-h5/docs/security/atu-prod-hardening-playbook.zh-CN.md) + +## 后续对我下指令的推荐说法 + +后续你切到 `atu-prod` 分支后,可以直接这样说: + +- 按 `atu-prod-hardening-playbook.zh-CN.md` 执行一遍 +- 先按手册同步全局加固能力,再扫描 `Activities / Ranking` +- 按手册把高风险页面优先迁移,并同步更新文档 + +看到这类指令后,我默认会: + +- 先核对分支当前状态 +- 再按这份文档逐项执行 +- 遇到结构差异时在不偏离目标的前提下做适配 +- 最后把实际执行结果写回文档 diff --git a/docs/security/frontend-hardening.md b/docs/security/frontend-hardening.md new file mode 100644 index 0000000..45a27da --- /dev/null +++ b/docs/security/frontend-hardening.md @@ -0,0 +1,295 @@ +# Frontend Hardening Notes + +## Purpose + +This document records the frontend-only hardening work added to this repository to raise the cost of scraping, asset reuse, and low-effort page reconstruction. + +The goal is to make abuse less convenient, not to claim absolute protection. + +## Scope + +All changes described here are limited to the frontend project. + +This work does not rely on: + +- backend API changes +- private OSS buckets +- signed image URLs +- server-side watermark rendering + +## What Was Added + +### 1. Protected page access control + +Protected routes are blocked outside the official app environment in production-like modes. + +Main files: + +- [src/utils/routeGuard.js](/D:/programs/likei-h5/src/utils/routeGuard.js) +- [src/utils/http.js](/D:/programs/likei-h5/src/utils/http.js) +- [src/config/security.js](/D:/programs/likei-h5/src/config/security.js) + +Behavior: + +- public routes still open normally +- protected routes redirect to `/#/not_app` outside the app +- protected API requests are rejected when the app bridge is unavailable + +### 2. Runtime anti-debug and anti-copy restrictions + +Protected pages now add a lightweight runtime restriction layer in production-like modes. + +Main files: + +- [src/utils/runtimeSecurity.js](/D:/programs/likei-h5/src/utils/runtimeSecurity.js) +- [src/main.js](/D:/programs/likei-h5/src/main.js) +- [src/App.vue](/D:/programs/likei-h5/src/App.vue) + +Behavior: + +- blocks common devtools shortcuts +- blocks right-click, drag, copy, and text selection on protected pages +- reduces long-press image save convenience +- adds page-level dynamic watermark overlays + +### 3. Protected asset URL layer for `Activities` and `Ranking` + +Most `Activities/` and `Ranking/` image references no longer expose raw OSS URLs directly in templates. + +Main files: + +- [src/config/imagePaths.js](/D:/programs/likei-h5/src/config/imagePaths.js) +- [src/utils/protectedAssets.js](/D:/programs/likei-h5/src/utils/protectedAssets.js) +- [src/utils/protectedAssetRuntime.js](/D:/programs/likei-h5/src/utils/protectedAssetRuntime.js) +- [src/directives/smartImage.js](/D:/programs/likei-h5/src/directives/smartImage.js) +- [src/utils/image/imageCacheManager.js](/D:/programs/likei-h5/src/utils/image/imageCacheManager.js) + +Behavior: + +- `getPngUrl()` and `getWebpUrl()` emit `likei-protected:...` +- runtime code resolves protected URLs only when rendering is needed +- most image rendering flows now end up using cached blob/object URLs +- even pages that missed `v-smart-img` still get a runtime fallback for protected image `src` + +### 4. `background-image` compatibility fixes + +Several high-risk pages previously exposed asset URLs through inline `background-image: url(...)`. + +Adjusted files: + +- [src/views/Activities/LesserBairam/index.vue](/D:/programs/likei-h5/src/views/Activities/LesserBairam/index.vue) +- [src/views/Activities/LuckyDollars/Season3/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season3/index.vue) +- [src/views/Activities/SpringFestival/index.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/index.vue) +- [src/views/Activities/SpringFestival/Task.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/Task.vue) +- [src/views/Ranking/KingAndQueen/TopList.vue](/D:/programs/likei-h5/src/views/Ranking/KingAndQueen/TopList.vue) + +Behavior: + +- protected helpers are used before writing image values into CSS +- fewer raw asset URLs appear directly in template or style strings + +### 5. Build artifact readability reduction + +Build output names were changed from readable page/component names to hash-based filenames. + +Main file: + +- [vite.config.js](/D:/programs/likei-h5/vite.config.js) + +Behavior: + +- JS chunk names no longer reveal page names such as `TopList`, `WeeklyStar`, or `Ranking` +- CSS files also use hash-based names +- static asset filenames are hash-based + +This does not stop reverse engineering, but it lowers the convenience of reading page structure from `dist/`. + +### 6. Production log stripping with a troubleshooting switch + +Production-like builds strip common debug output, while still supporting a keep-logs build for app-side troubleshooting. + +Main files: + +- [vite.config.js](/D:/programs/likei-h5/vite.config.js) +- [package.json](/D:/programs/likei-h5/package.json) +- [scripts/build-with-version.js](/D:/programs/likei-h5/scripts/build-with-version.js) + +Behavior: + +- `production` and `production-atu` builds remove `console.log` +- `production` and `production-atu` builds remove `console.debug` +- `production` and `production-atu` builds remove `console.info` +- production-like builds remove `debugger` +- `console.warn` and `console.error` are preserved +- Jenkins can keep using the original default build commands + +Useful commands: + +```bash +npm run build:prod +npm run build:prod:logs +npm run build:atu-prod +npm run build:atu-prod:logs +``` + +### 7. Deferred main DOM mounting for selected high-risk pages + +Some activity and ranking pages previously used `v-show="!isLoading"` for the main content. That kept the full DOM tree mounted early, which made initial inspection easier and also caused some observer timing issues. + +These pages were updated to mount their main content with `v-if="!isLoading"` instead: + +- [src/views/Activities/LoginReward/index.vue](/D:/programs/likei-h5/src/views/Activities/LoginReward/index.vue) +- [src/views/Activities/LuckyDollars/Season4/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season4/index.vue) +- [src/views/Activities/SpringFestival/index.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/index.vue) +- [src/views/Ranking/Couple/index.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/index.vue) +- [src/views/Ranking/Overall/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Overall/Ranking.vue) + +Extra handling was added where needed: + +- pages with `IntersectionObserver` targets now register observers only after `await nextTick()` +- preload completion still clears `isLoading` in `finally`, so preload failures do not leave the page permanently blank +- this keeps the original lazy-load behavior more stable after switching from `v-show` to `v-if` + +Important limit: + +This is only a weak hardening step. It reduces early DOM exposure and makes initial inspection less direct, but it does not stop scraping once the page is fully rendered on the client. + +### 8. Canvas-rendered decorative background stacks + +The shared background layer component now supports a canvas rendering mode for stacked background images. + +Main file: + +- [src/components/BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue) + +Current high-risk pages using this mode: + +- [src/views/Activities/LesserBairam/index.vue](/D:/programs/likei-h5/src/views/Activities/LesserBairam/index.vue) +- [src/views/Activities/LoginReward/index.vue](/D:/programs/likei-h5/src/views/Activities/LoginReward/index.vue) +- [src/views/Activities/LuckyDollars/Season3/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season3/index.vue) +- [src/views/Ranking/Couple/index.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/index.vue) +- [src/views/Activities/LuckyDollars/Season4/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season4/index.vue) +- [src/views/Activities/SpringFestival/index.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/index.vue) +- [src/views/Ranking/GamesKing/index.vue](/D:/programs/likei-h5/src/views/Ranking/GamesKing/index.vue) +- [src/views/Ranking/KingAndQueen/TopList.vue](/D:/programs/likei-h5/src/views/Ranking/KingAndQueen/TopList.vue) +- [src/views/Ranking/Overall/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Overall/Ranking.vue) +- [src/views/Ranking/WeeklyStar/WeeklyStar.vue](/D:/programs/likei-h5/src/views/Ranking/WeeklyStar/WeeklyStar.vue) + +Behavior: + +- stacked decorative backgrounds are drawn into a single canvas instead of a readable `` list +- most page-level decorative background structure becomes less obvious in Elements inspection +- protected asset resolution still goes through the existing runtime/cache pipeline + +This does not hide the pixels from a determined attacker, but it reduces direct DOM readability and low-effort structure copying. + +### 9. Stronger production minification and selector cleanup + +Production-like builds now use stricter `esbuild` minification flags, and a small set of highly readable page-local class names was reduced on sensitive pages. + +Main files: + +- [vite.config.js](/D:/programs/likei-h5/vite.config.js) +- [src/views/Ranking/Couple/index.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/index.vue) +- [src/views/Activities/LuckyDollars/Season4/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season4/index.vue) +- [src/views/Activities/SpringFestival/index.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/index.vue) +- [src/views/Activities/SpringFestival/Ranking.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/Ranking.vue) +- [src/views/Ranking/Overall/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Overall/Ranking.vue) +- [src/views/Ranking/WeeklyStar/WeeklyStar.vue](/D:/programs/likei-h5/src/views/Ranking/WeeklyStar/WeeklyStar.vue) + +Behavior: + +- production-like builds explicitly minify identifiers, syntax, and whitespace +- legal comments are removed from build output +- some obvious local selector names were replaced with shorter neutral names +- selector cleanup now also covers obvious ranking navigation and history labels on additional `Activities` and `Ranking` pages +- `SpringFestival/Ranking` now waits for `nextTick()` before observing its load sentinel and disconnects the observer on unmount + +## What This Helps Against + +This frontend-only hardening is helpful against: + +- direct browser opening of protected routes +- low-effort DOM scraping +- copying image URLs from templates or page source +- reading obvious page intent from build artifact names +- simple right-click, drag-save, long-press, and casual copying +- basic page reconstruction that depends on immediately available DOM structure + +## Current Limits + +These changes still do not fully prevent: + +- network-level image capture +- devtools users with enough time and persistence +- custom WebView, automation, or hook-based extraction +- rebuilding the same layout after visual inspection +- extracting DOM, CSS, and JS from any client that is allowed to render the page + +Important rule: + +If the client can render the page, the client must receive enough information to reproduce the page. + +That is why frontend-only hardening can only increase cost, not provide absolute protection. + +## Recommended Testing + +### Protected route blocking + +Use: + +```bash +npm run dev:prod +``` + +or: + +```bash +npm run build +npm run preview +``` + +Expected: + +- protected routes redirect to `/#/not_app` outside the app +- public routes still open normally + +### Asset and page rendering checks + +In production-like mode, verify: + +- `Activities` and `Ranking` pages still render correctly +- protected page images still display normally +- protected asset URLs are less visible in runtime DOM and build output +- pages switched from `v-show` to `v-if` still render after preload completes +- pages with load-more observers still trigger lazy loading correctly + +### Build output checks + +Run: + +```bash +npm run build +``` + +Expected: + +- `dist/js` filenames are hash-based +- `dist/css` filenames are hash-based +- no obvious page names appear in bundle filenames +- production bundles no longer retain `console.log`, `console.debug`, `console.info`, or `debugger` + +## Suggested Next Steps + +If staying frontend-only: + +- move more decorative shells from plain DOM/CSS into canvas rendering +- reduce semantic structure in the most sensitive event pages +- keep replacing remaining raw `background-image` and direct image flows with protected helpers + +If minimal backend support becomes acceptable later: + +- move sensitive OSS paths to private read +- issue short-lived signed URLs +- watermark sensitive images per user on the server side +- validate app session or device identity before returning sensitive assets diff --git a/docs/security/frontend-hardening.zh-CN.md b/docs/security/frontend-hardening.zh-CN.md new file mode 100644 index 0000000..ddd6f0f --- /dev/null +++ b/docs/security/frontend-hardening.zh-CN.md @@ -0,0 +1,324 @@ +# 前端加固说明 + +## 目的 + +这份文档用于记录当前仓库内已经落地的前端侧防扒取、防复用加固方案。 + +核心目标是提高扒图、抄页面、低成本复刻的门槛,而不是宣称“前端已经绝对防住”。 + +如需在后续切换到 `atu-prod` 分支后复用整套处理,请直接参考: + +- [docs/security/atu-prod-hardening-playbook.zh-CN.md](/D:/programs/likei-h5/docs/security/atu-prod-hardening-playbook.zh-CN.md) + +## 范围 + +本次加固严格限制在当前前端项目内完成。 + +当前方案不依赖: + +- 后端接口改造 +- OSS 私有读 +- 短时签名 URL +- 服务端动态水印 + +## 已落地能力 + +### 1. 受保护页面访问限制 + +生产相关模式下,受保护页面要求运行在官方 App 环境中。 + +主要文件: + +- [src/utils/routeGuard.js](/D:/programs/likei-h5/src/utils/routeGuard.js) +- [src/utils/http.js](/D:/programs/likei-h5/src/utils/http.js) +- [src/config/security.js](/D:/programs/likei-h5/src/config/security.js) + +表现: + +- 公共页面仍可正常访问 +- 受保护页面在非 App 环境下会跳转到 `/#/not_app` +- 受保护页面内的接口请求在缺少 App bridge 时会被拒绝 + +### 2. 运行时防调试、防复制与页面水印 + +对受保护页面增加了一层轻量运行时限制。 + +主要文件: + +- [src/utils/runtimeSecurity.js](/D:/programs/likei-h5/src/utils/runtimeSecurity.js) +- [src/main.js](/D:/programs/likei-h5/src/main.js) +- [src/App.vue](/D:/programs/likei-h5/src/App.vue) + +表现: + +- 屏蔽常见开发者工具快捷键 +- 屏蔽右键、拖拽、复制、文本选择等交互 +- 降低长按保存图片的便利性 +- 给受保护页面增加动态水印覆盖 + +### 3. `Activities` / `Ranking` 图片受保护地址层 + +`Activities/` 和 `Ranking/` 目录下的大部分图片,不再直接以真实 OSS 地址出现在模板中。 + +主要文件: + +- [src/config/imagePaths.js](/D:/programs/likei-h5/src/config/imagePaths.js) +- [src/utils/protectedAssets.js](/D:/programs/likei-h5/src/utils/protectedAssets.js) +- [src/utils/protectedAssetRuntime.js](/D:/programs/likei-h5/src/utils/protectedAssetRuntime.js) +- [src/directives/smartImage.js](/D:/programs/likei-h5/src/directives/smartImage.js) +- [src/utils/image/imageCacheManager.js](/D:/programs/likei-h5/src/utils/image/imageCacheManager.js) + +表现: + +- `getPngUrl()` / `getWebpUrl()` 会先输出 `likei-protected:...` +- 运行时只有在真正渲染时才解析成真实地址 +- 大部分图片最终会走缓存并转换成 blob/object URL +- 即使个别页面漏了 `v-smart-img`,运行时也有兜底解析 + +### 4. `background-image` 泄露点补丁 + +部分重点页面之前仍通过内联 `background-image: url(...)` 直接暴露真实地址,这类点位已经做了兼容收口。 + +已调整文件: + +- [src/views/Activities/LesserBairam/index.vue](/D:/programs/likei-h5/src/views/Activities/LesserBairam/index.vue) +- [src/views/Activities/LuckyDollars/Season3/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season3/index.vue) +- [src/views/Activities/SpringFestival/index.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/index.vue) +- [src/views/Activities/SpringFestival/Task.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/Task.vue) +- [src/views/Ranking/KingAndQueen/TopList.vue](/D:/programs/likei-h5/src/views/Ranking/KingAndQueen/TopList.vue) + +表现: + +- 写入 CSS 前先通过受保护地址辅助方法解析 +- 模板和样式字符串里直接出现真实图链的情况更少 + +### 5. 构建产物可读性降低 + +构建输出文件名从可读页面名改成了哈希命名。 + +主要文件: + +- [vite.config.js](/D:/programs/likei-h5/vite.config.js) + +表现: + +- JS chunk 文件名不再直接暴露 `TopList`、`WeeklyStar`、`Ranking` 等页面名 +- CSS 文件名也改为哈希命名 +- 静态资源文件名同样使用哈希 + +这一步不能阻止逆向,但能降低别人直接从 `dist/` 推断页面结构的便利性。 + +### 6. 生产包自动清理日志,并保留可切换开关 + +生产相关打包模式下,会自动清掉常见调试日志;同时保留一个方便排查 App 内问题的“保留日志”构建开关。 + +主要文件: + +- [vite.config.js](/D:/programs/likei-h5/vite.config.js) +- [package.json](/D:/programs/likei-h5/package.json) +- [scripts/build-with-version.js](/D:/programs/likei-h5/scripts/build-with-version.js) + +表现: + +- `production` 和 `production-atu` 下自动移除 `console.log` +- `production` 和 `production-atu` 下自动移除 `console.debug` +- `production` 和 `production-atu` 下自动移除 `console.info` +- 生产相关模式下自动移除 `debugger` +- `console.warn` 和 `console.error` 保留,方便线上排查 +- Jenkins 仍可继续使用原有默认打包命令 + +常用命令: + +```bash +npm run build:prod +npm run build:prod:logs +npm run build:atu-prod +npm run build:atu-prod:logs +``` + +### 7. 部分高风险页面改为延迟挂载主内容 DOM + +部分活动页和榜单页原来使用 `v-show="!isLoading"` 控制主内容,这会让完整 DOM 树在较早阶段就已经挂载出来,也容易和 `IntersectionObserver` 的注册时序冲突。 + +现在这几页已经改成 `v-if="!isLoading"` 再挂载主内容: + +- [src/views/Activities/LoginReward/index.vue](/D:/programs/likei-h5/src/views/Activities/LoginReward/index.vue) +- [src/views/Activities/LuckyDollars/Season4/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season4/index.vue) +- [src/views/Activities/SpringFestival/index.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/index.vue) +- [src/views/Ranking/Couple/index.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/index.vue) +- [src/views/Ranking/Overall/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Overall/Ranking.vue) + +同步补了两类兜底: + +- 对依赖 `IntersectionObserver` 的页面,在 `await nextTick()` 之后再注册观察器 +- 预加载结束逻辑仍然放在 `finally` 中,避免预加载失败时页面长期空白 + +这一步的作用要明确: + +- 可以减少初始阶段 DOM 直接暴露的程度 +- 可以让“页面刚进来就看 Elements 面板抄结构”变得没那么直接 +- 不能阻止页面渲染完成后的继续抓取 + +也就是说,它属于弱加固,而不是决定性的防扒手段。 + +### 8. 装饰背景堆图改为可选 canvas 渲染 + +公共背景组件现在支持可选的 canvas 渲染模式,适合拿来处理活动页和榜单页那种“纯装饰背景层层堆图”的场景。 + +主要文件: + +- [src/components/BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue) + +当前已接入的高风险页面: + +- [src/views/Activities/LesserBairam/index.vue](/D:/programs/likei-h5/src/views/Activities/LesserBairam/index.vue) +- [src/views/Activities/LoginReward/index.vue](/D:/programs/likei-h5/src/views/Activities/LoginReward/index.vue) +- [src/views/Activities/LuckyDollars/Season3/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season3/index.vue) +- [src/views/Ranking/Couple/index.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/index.vue) +- [src/views/Activities/LuckyDollars/Season4/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season4/index.vue) +- [src/views/Activities/SpringFestival/index.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/index.vue) +- [src/views/Ranking/GamesKing/index.vue](/D:/programs/likei-h5/src/views/Ranking/GamesKing/index.vue) +- [src/views/Ranking/KingAndQueen/TopList.vue](/D:/programs/likei-h5/src/views/Ranking/KingAndQueen/TopList.vue) +- [src/views/Ranking/Overall/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Overall/Ranking.vue) +- [src/views/Ranking/WeeklyStar/WeeklyStar.vue](/D:/programs/likei-h5/src/views/Ranking/WeeklyStar/WeeklyStar.vue) + +表现: + +- 多张纯装饰背景图会先合成到单个 canvas 中,而不是在 DOM 里直接挂一串 `` +- 从 Elements 面板直接看页面背景结构会比之前更不直观 +- 受保护图片地址仍然继续走现有的运行时解析和缓存链路 + +这一步不能阻止有经验的人继续拿到像素内容,但能明显降低“直接看 DOM 抄背景结构”的便利度。 + +### 9. 生产构建混淆再收紧,并补一轮明显语义类名收口 + +生产相关构建模式现在显式开启更严格的 `esbuild` 压缩参数,同时针对重点页面收了一轮最显眼的本地类名。 + +主要文件: + +- [vite.config.js](/D:/programs/likei-h5/vite.config.js) +- [src/views/Ranking/Couple/index.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/index.vue) +- [src/views/Activities/LuckyDollars/Season4/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season4/index.vue) +- [src/views/Activities/SpringFestival/index.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/index.vue) +- [src/views/Activities/SpringFestival/Ranking.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/Ranking.vue) +- [src/views/Ranking/Overall/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Overall/Ranking.vue) +- [src/views/Ranking/WeeklyStar/WeeklyStar.vue](/D:/programs/likei-h5/src/views/Ranking/WeeklyStar/WeeklyStar.vue) + +表现: + +- 生产相关构建会显式压缩标识符、语法和空白 +- 构建产物中的合法注释会被去掉 +- 一部分过于直白的页面本地类名已经换成更短、更弱语义的名字 + +### 10. `Ranking/Couple` 注释修复与背景组件说明补充 + +近期对 [src/views/Ranking/Couple/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/Ranking.vue) 做了一次编码与注释清理: + +- 清除了上一轮编辑中遗留的乱码注释 +- 将核心业务注释统一收口为中文,便于后续维护和分支迁移 +- 保留当前逻辑不变,只修正文案可读性与排查体验 + +同时补充说明两个容易混淆的背景层工具点: + +- [src/components/BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue) 的 `useCanvas` 是显式开关,不默认全局开启 +- 原因是 `BackgroundLayer` 是公共组件,默认全开会把所有背景层都切到 canvas 路径,带来额外的图片加载、重绘和兼容风险 +- 当前只在 `Activities / Ranking` 这类高风险页面显式传入 `:useCanvas=\"true\"`,把成本控制在需要加固的范围内 +- [src/utils/protectedAssets.js](/D:/programs/likei-h5/src/utils/protectedAssets.js) 中的 `toCssBackgroundImage()` 用于把 `likei-protected:` 地址还原成 CSS 可用的 `background-image: url(...)` +- 这个方法是兼容桥接,不是额外的加密层,主要服务于仍需使用内联背景图的场景 + +如果后续需要在 `atu-prod` 分支做同类处理,建议优先对照这几个文件迁移: + +- [src/views/Ranking/Couple/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/Ranking.vue) +- [src/components/BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue) +- [src/utils/protectedAssets.js](/D:/programs/likei-h5/src/utils/protectedAssets.js) +- [docs/security/frontend-hardening.zh-CN.md](/D:/programs/likei-h5/docs/security/frontend-hardening.zh-CN.md) +- [docs/security/frontend-hardening.zh-CN.updates.md](/D:/programs/likei-h5/docs/security/frontend-hardening.zh-CN.updates.md) + +## 这套方案能挡住什么 + +当前前端加固,对以下场景有一定抬门槛作用: + +- 普通浏览器直接打开受保护页面 +- 低门槛 DOM 抓取 +- 从模板或页面源码里直接抄图片地址 +- 从可读的构建产物文件名里反推页面用途 +- 简单右键、拖拽、长按保存 +- 依赖“页面初始 DOM 直接可见”的低成本抄结构方式 + +## 当前边界 + +这套改动仍然无法彻底防住以下情况: + +- 通过 Network 面板或抓包获取图片请求 +- 有经验的人持续分析前端运行时代码 +- 自定义 WebView、自动化脚本、Hook 等方式截获资源 +- 仅凭视觉观察后重新临摹布局和样式 +- 从一个本来就被允许渲染页面的客户端中提取 DOM、CSS、JS + +一个关键原则是: + +只要客户端能把页面渲染出来,客户端就一定拿到了足够还原页面的信息。 + +所以前端侧能做的,本质上只有“提高成本”,而不是“绝对防住”。 + +## 推荐测试方式 + +### 受保护页面拦截 + +可使用: + +```bash +npm run dev:prod +``` + +或: + +```bash +npm run build +npm run preview +``` + +预期结果: + +- 非 App 环境下访问受保护页面会跳转到 `/#/not_app` +- 公共页面仍可正常访问 + +### 页面与图片链路验证 + +在接近生产环境的模式下检查: + +- `Activities` / `Ranking` 页面是否仍能正常展示 +- 受保护图片是否仍能正常显示 +- 运行时 DOM 和构建产物里是否更少直接暴露真实图链 +- 本次从 `v-show` 切到 `v-if` 的页面,是否仍能在预加载结束后正常渲染 +- 依赖触底加载的页面,滚动加载是否仍然正常 + +### 构建产物验证 + +执行: + +```bash +npm run build +``` + +预期结果: + +- `dist/js` 文件名以哈希为主 +- `dist/css` 文件名以哈希为主 +- 构建产物文件名中不再直观出现页面名 +- 生产包中不再保留 `console.log`、`console.debug`、`console.info` 和 `debugger` + +## 后续仍可继续推进的方向 + +如果继续坚持只改前端项目,可以优先考虑: + +- 把更敏感的装饰层、外框层继续往 `canvas` 渲染收口 +- 继续减少关键活动页中可直接阅读的结构语义 +- 继续替换剩余直接 `background-image` 或直出图片链路 + +如果后续允许少量后端配合,优先级更高的是: + +- 敏感 OSS 目录改成私有读 +- 使用短时签名 URL +- 给敏感图片做按用户生成的动态水印 +- 资源下发前校验 App 会话或设备身份 diff --git a/docs/security/frontend-hardening.zh-CN.updates.md b/docs/security/frontend-hardening.zh-CN.updates.md new file mode 100644 index 0000000..a99fd9c --- /dev/null +++ b/docs/security/frontend-hardening.zh-CN.updates.md @@ -0,0 +1,58 @@ +# 前端加固增量记录 + +这份文档用于补充 [frontend-hardening.zh-CN.md](/D:/programs/likei-h5/docs/security/frontend-hardening.zh-CN.md) 在最近一轮 `Activities / Ranking` 加固中的新增改动。 + +## 本轮补充 + +- 生产构建混淆进一步收紧,`esbuild` 在生产相关模式下显式开启了更强的标识符、语法和空白压缩,并去除了合法注释输出。相关文件在 [vite.config.js](/D:/programs/likei-h5/vite.config.js)。 +- 公共背景组件 [BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue) 新增了 `useCanvas` 模式,装饰性多层背景会优先绘制到单个 canvas,而不是在 DOM 中直接暴露多张 ``。 +- `canvas` 背景已扩展到更多高风险页面: + [LesserBairam/index.vue](/D:/programs/likei-h5/src/views/Activities/LesserBairam/index.vue), + [LoginReward/index.vue](/D:/programs/likei-h5/src/views/Activities/LoginReward/index.vue), + [LuckyDollars/Season3/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season3/index.vue), + [LuckyDollars/Season4/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season4/index.vue), + [SpringFestival/index.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/index.vue), + [Ranking/Couple/index.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/index.vue), + [Ranking/GamesKing/index.vue](/D:/programs/likei-h5/src/views/Ranking/GamesKing/index.vue), + [Ranking/KingAndQueen/TopList.vue](/D:/programs/likei-h5/src/views/Ranking/KingAndQueen/TopList.vue), + [Ranking/Overall/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Overall/Ranking.vue), + [Ranking/WeeklyStar/WeeklyStar.vue](/D:/programs/likei-h5/src/views/Ranking/WeeklyStar/WeeklyStar.vue)。 +- 第二轮语义类名收口已继续推进,当前已覆盖: + [Ranking/Couple/index.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/index.vue), + [LuckyDollars/Season4/index.vue](/D:/programs/likei-h5/src/views/Activities/LuckyDollars/Season4/index.vue), + [SpringFestival/index.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/index.vue), + [SpringFestival/Ranking.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/Ranking.vue), + [Ranking/Overall/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Overall/Ranking.vue), + [Ranking/WeeklyStar/WeeklyStar.vue](/D:/programs/likei-h5/src/views/Ranking/WeeklyStar/WeeklyStar.vue)。 +- [SpringFestival/Ranking.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/Ranking.vue) 已额外做了时序加固: + 组件会在 `nextTick()` 之后再注册 `IntersectionObserver`,并在 `onUnmounted` 时执行 `disconnect()`,避免监听节点还没稳定挂载时就注册,或页面切换后残留观察器。 +- [Ranking/Couple/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/Ranking.vue) 已清理上一轮遗留的乱码注释,并统一替换为中文注释,方便后续排查与跨分支迁移。 +- [BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue) 的 `useCanvas` 继续保持显式开启,而没有改成默认值 `true`: + 这是为了避免把所有背景层都切到 canvas 渲染路径,给普通页面增加不必要的加载、绘制和重绘成本。 +- [protectedAssets.js](/D:/programs/likei-h5/src/utils/protectedAssets.js) 中的 `toCssBackgroundImage()` 作用也已明确: + 它负责把 `likei-protected:` 地址还原为 CSS 可直接使用的 `url(...)` 字符串,主要用于必须保留 `background-image` 的内联样式场景。 + +## 当前边界 + +- 这轮改动依然属于“提高门槛”,不是“彻底防住”。 +- `canvas` 能降低从 Elements 面板直接抄背景结构的便利度,但防不住抓包、运行时 Hook 或最终像素提取。 +- 类名和结构收口能降低产物可读性,但不能阻止有经验的人在页面完全渲染后继续还原布局。 + +## 验证建议 + +- 使用 `npm run build` 验证生产包可正常构建。 +- 使用 `npm run dev:prod` 或 `npm run preview` 验证活动页、榜单页在生产态逻辑下仍能正常显示。 +- 重点检查 `SpringFestival`、`Season4`、`WeeklyStar`、`Overall` 这几类近期改动较多的页面。 + +## `atu-prod` 迁移建议 + +- 如果后续要在 `atu-prod` 分支复用同类处理,优先迁移本轮涉及的三个核心文件: + [Ranking/Couple/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/Ranking.vue)、 + [BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue)、 + [protectedAssets.js](/D:/programs/likei-h5/src/utils/protectedAssets.js)。 +- 同步迁移文档文件,避免分支间的实现与说明脱节: + [frontend-hardening.zh-CN.md](/D:/programs/likei-h5/docs/security/frontend-hardening.zh-CN.md)、 + [frontend-hardening.zh-CN.updates.md](/D:/programs/likei-h5/docs/security/frontend-hardening.zh-CN.updates.md)。 +- 后续如果主分支继续更新了这些文档,切到目标分支后可直接执行: + `npm run docs:sync:security -- master` +- 该命令依赖脚本 [scripts/sync-security-docs.js](/D:/programs/likei-h5/scripts/sync-security-docs.js),前提是主分支里的文档版本已经提交到 Git。 diff --git a/package.json b/package.json index bb325a3..94f1ed3 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,12 @@ "build:dev": "vite build --mode development && node scripts/generate-version.js", "build:test": "vite build --mode test && node scripts/generate-version.js", "build:prod": "vite build --mode production && node scripts/generate-version.js", + "build:prod:logs": "node scripts/build-with-version.js production --keep-logs", "build:atu-prod": "vite build --mode production-atu && node scripts/generate-version.js", + "build:atu-prod:logs": "node scripts/build-with-version.js production-atu --keep-logs", "preview": "vite preview", - "lint": "eslint . --fix" + "lint": "eslint . --fix", + "docs:sync:security": "node scripts/sync-security-docs.js" }, "dependencies": { "@vueuse/core": "^13.9.0", diff --git a/scripts/build-with-version.js b/scripts/build-with-version.js new file mode 100644 index 0000000..ea0fa1d --- /dev/null +++ b/scripts/build-with-version.js @@ -0,0 +1,36 @@ +import { spawnSync } from 'node:child_process' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const args = process.argv.slice(2) +const mode = args[0] || 'production' +const keepLogs = args.includes('--keep-logs') + +const viteBin = resolve(__dirname, '../node_modules/vite/bin/vite.js') +const versionScript = resolve(__dirname, './generate-version.js') + +const buildEnv = { + ...process.env, + LIKEI_KEEP_BUILD_LOGS: keepLogs ? 'true' : 'false', +} + +function runNodeScript(scriptPath, scriptArgs = []) { + const result = spawnSync(process.execPath, [scriptPath, ...scriptArgs], { + stdio: 'inherit', + env: buildEnv, + }) + + if (result.error) { + throw result.error + } + + if (result.status !== 0) { + process.exit(result.status ?? 1) + } +} + +runNodeScript(viteBin, ['build', '--mode', mode]) +runNodeScript(versionScript) diff --git a/scripts/sync-security-docs.js b/scripts/sync-security-docs.js new file mode 100644 index 0000000..a7482a7 --- /dev/null +++ b/scripts/sync-security-docs.js @@ -0,0 +1,72 @@ +import { execFileSync } from 'node:child_process' +import { mkdirSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' + +const DOC_FILES = [ + 'docs/security/frontend-hardening.md', + 'docs/security/frontend-hardening.zh-CN.md', + 'docs/security/frontend-hardening.zh-CN.updates.md', + 'docs/security/atu-prod-hardening-playbook.zh-CN.md', +] + +function printHelp() { + console.log(` +用法: + node scripts/sync-security-docs.js [source-ref] + +说明: + 将安全文档从指定分支或提交同步到当前工作区。 + 默认 source-ref 为 master。 + +示例: + node scripts/sync-security-docs.js + node scripts/sync-security-docs.js master + node scripts/sync-security-docs.js origin/master + npm run docs:sync:security -- master + +注意: + 只有当 source-ref 中已经提交了这些文档时,才能成功同步。 +`.trim()) +} + +function readFileFromGit(sourceRef, relativePath) { + try { + return execFileSync('git', ['show', `${sourceRef}:${relativePath}`], { + encoding: 'utf8', + maxBuffer: 10 * 1024 * 1024, + }) + } catch (error) { + const message = [ + `无法从 ${sourceRef} 读取 ${relativePath}。`, + '请先确认源分支中已经提交了这些文档,再执行同步。', + ].join('\n') + throw new Error(message, { cause: error }) + } +} + +function writeWorkspaceFile(relativePath, content) { + const absolutePath = join(process.cwd(), relativePath) + mkdirSync(dirname(absolutePath), { recursive: true }) + writeFileSync(absolutePath, content, 'utf8') +} + +function main() { + const sourceRef = process.argv[2] || 'master' + + if (sourceRef === '--help' || sourceRef === '-h') { + printHelp() + return + } + + console.log(`开始从 ${sourceRef} 同步安全文档...`) + + for (const relativePath of DOC_FILES) { + const content = readFileFromGit(sourceRef, relativePath) + writeWorkspaceFile(relativePath, content) + console.log(`已同步: ${relativePath}`) + } + + console.log('安全文档同步完成。') +} + +main() diff --git a/src/App.vue b/src/App.vue index 1854aea..e99f3ab 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,18 +1,47 @@ @@ -30,10 +59,9 @@ body { padding: 0; width: 100%; height: 100%; - /* 设置字体和行高 */ font-family: 'Baloo 2', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - line-height: 1.4; /* 调整行高 */ + line-height: 1.4; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-tap-highlight-color: transparent; @@ -48,4 +76,37 @@ body { font-family: 'Baloo 2', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } + +#app.protected-page img { + -webkit-user-drag: none; + -webkit-touch-callout: none; + user-select: none; +} + +.security-watermark { + position: fixed; + inset: 0; + z-index: 9999; + pointer-events: none; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 48px 24px; + padding: 40px 16px; + opacity: 0.11; + overflow: hidden; +} + +.security-watermark span { + display: flex; + align-items: center; + justify-content: center; + min-height: 96px; + color: #0f172a; + font-size: 14px; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; + transform: rotate(-24deg); + white-space: nowrap; +} diff --git a/src/components/BackgroundLayer.vue b/src/components/BackgroundLayer.vue index a42079e..70a0b86 100644 --- a/src/components/BackgroundLayer.vue +++ b/src/components/BackgroundLayer.vue @@ -1,6 +1,14 @@