chore: 取消动态水印

This commit is contained in:
hzj 2026-04-21 18:23:45 +08:00
parent 816f4a1842
commit 073c61e6bb
5 changed files with 60 additions and 571 deletions

View File

@ -1,361 +0,0 @@
# `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. 是否仍有大量直接 `<img :src="getPngUrl(...)" />`
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`
- 按手册把高风险页面优先迁移,并同步更新文档
看到这类指令后,我默认会:
- 先核对分支当前状态
- 再按这份文档逐项执行
- 遇到结构差异时在不偏离目标的前提下做适配
- 最后把实际执行结果写回文档

View File

@ -2,20 +2,20 @@
## Purpose ## 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. This document records the frontend-only hardening currently applied on the main branch 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. The goal is to make abuse less convenient, not to claim absolute protection.
## Scope ## Scope
All changes described here are limited to the frontend project. All changes described here stay within the frontend project.
This work does not rely on: They do not rely on:
- backend API changes - backend API changes
- private OSS buckets - private OSS buckets
- signed image URLs - signed image URLs
- server-side watermark rendering - server-side image rewriting
## What Was Added ## What Was Added
@ -37,7 +37,7 @@ Behavior:
### 2. Runtime anti-debug and anti-copy restrictions ### 2. Runtime anti-debug and anti-copy restrictions
Protected pages now add a lightweight runtime restriction layer in production-like modes. Protected pages add a lightweight runtime restriction layer in production-like modes.
Main files: Main files:
@ -50,7 +50,6 @@ Behavior:
- blocks common devtools shortcuts - blocks common devtools shortcuts
- blocks right-click, drag, copy, and text selection on protected pages - blocks right-click, drag, copy, and text selection on protected pages
- reduces long-press image save convenience - reduces long-press image save convenience
- adds page-level dynamic watermark overlays
### 3. Protected asset URL layer for `Activities` and `Ranking` ### 3. Protected asset URL layer for `Activities` and `Ranking`
@ -69,7 +68,7 @@ Behavior:
- `getPngUrl()` and `getWebpUrl()` emit `likei-protected:...` - `getPngUrl()` and `getWebpUrl()` emit `likei-protected:...`
- runtime code resolves protected URLs only when rendering is needed - runtime code resolves protected URLs only when rendering is needed
- most image rendering flows now end up using cached blob/object URLs - 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` - pages that missed `v-smart-img` still get a runtime fallback for protected image `src`
### 4. `background-image` compatibility fixes ### 4. `background-image` compatibility fixes
@ -102,11 +101,9 @@ Behavior:
- CSS files also use hash-based names - CSS files also use hash-based names
- static asset filenames are hash-based - 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 ### 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. Production-like builds strip common debug output while still supporting a keep-logs build for app-side troubleshooting.
Main files: Main files:
@ -116,9 +113,9 @@ Main files:
Behavior: Behavior:
- `production` and `production-atu` builds remove `console.log` - `production` builds remove `console.log`
- `production` and `production-atu` builds remove `console.debug` - `production` builds remove `console.debug`
- `production` and `production-atu` builds remove `console.info` - `production` builds remove `console.info`
- production-like builds remove `debugger` - production-like builds remove `debugger`
- `console.warn` and `console.error` are preserved - `console.warn` and `console.error` are preserved
- Jenkins can keep using the original default build commands - Jenkins can keep using the original default build commands
@ -128,8 +125,6 @@ Useful commands:
```bash ```bash
npm run build:prod npm run build:prod
npm run build:prod:logs 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 ### 7. Deferred main DOM mounting for selected high-risk pages
@ -148,15 +143,10 @@ Extra handling was added where needed:
- pages with `IntersectionObserver` targets now register observers only after `await nextTick()` - 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 - 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 ### 8. Canvas-rendered decorative background stacks
The shared background layer component now supports a canvas rendering mode for stacked background images. The shared background layer component supports a canvas rendering mode for stacked background images.
Main file: Main file:
@ -181,11 +171,9 @@ Behavior:
- most page-level decorative background structure becomes less obvious in Elements inspection - most page-level decorative background structure becomes less obvious in Elements inspection
- protected asset resolution still goes through the existing runtime/cache pipeline - 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 ### 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. Production-like builds use stricter `esbuild` minification flags, and a small set of highly readable page-local class names was reduced on sensitive pages.
Main files: Main files:
@ -197,13 +185,19 @@ Main files:
- [src/views/Ranking/Overall/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Overall/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) - [src/views/Ranking/WeeklyStar/WeeklyStar.vue](/D:/programs/likei-h5/src/views/Ranking/WeeklyStar/WeeklyStar.vue)
### 10. `Ranking/Couple` comment cleanup and helper clarification
Recent cleanup also covered:
- [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)
Behavior: Behavior:
- production-like builds explicitly minify identifiers, syntax, and whitespace - garbled comments in `Ranking/Couple/Ranking.vue` were cleaned up and unified into Chinese comments
- legal comments are removed from build output - `useCanvas` remains an explicit opt-in instead of becoming a global default
- some obvious local selector names were replaced with shorter neutral names - `toCssBackgroundImage()` stays as the CSS bridge for protected asset URLs
- 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 ## What This Helps Against
@ -226,11 +220,7 @@ These changes still do not fully prevent:
- rebuilding the same layout after visual inspection - rebuilding the same layout after visual inspection
- extracting DOM, CSS, and JS from any client that is allowed to render the page - 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 it. Frontend-only hardening can only increase cost, not provide absolute protection.
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 ## Recommended Testing
@ -249,11 +239,6 @@ npm run build
npm run preview npm run preview
``` ```
Expected:
- protected routes redirect to `/#/not_app` outside the app
- public routes still open normally
### Asset and page rendering checks ### Asset and page rendering checks
In production-like mode, verify: In production-like mode, verify:
@ -279,17 +264,3 @@ Expected:
- no obvious page names appear in bundle filenames - no obvious page names appear in bundle filenames
- production bundles no longer retain `console.log`, `console.debug`, `console.info`, or `debugger` - 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

View File

@ -2,24 +2,18 @@
## 目的 ## 目的
这份文档用于记录当前仓库内已经落地的前端侧防扒取、防复用加固方案。 这份文档用于记录当前主分支已经落地的前端防扒取加固方案。
核心目标是提高扒图、抄页面、低成本复刻的门槛,而不是宣称“前端已经绝对防住”。 核心目标是提高扒图、抄页面、低成本复刻的门槛,而不是宣称“前端已经绝对防住”。
如需在后续切换到 `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 私有读 - 私有 OSS
- 短时签名 URL - 短时签名 URL
- 服务端动态水印 - 服务端动态出图
## 已落地能力 ## 已落地能力
@ -39,7 +33,7 @@
- 受保护页面在非 App 环境下会跳转到 `/#/not_app` - 受保护页面在非 App 环境下会跳转到 `/#/not_app`
- 受保护页面内的接口请求在缺少 App bridge 时会被拒绝 - 受保护页面内的接口请求在缺少 App bridge 时会被拒绝
### 2. 运行时防调试、防复制与页面水印 ### 2. 运行时防调试与防复制限制
对受保护页面增加了一层轻量运行时限制。 对受保护页面增加了一层轻量运行时限制。
@ -54,7 +48,6 @@
- 屏蔽常见开发者工具快捷键 - 屏蔽常见开发者工具快捷键
- 屏蔽右键、拖拽、复制、文本选择等交互 - 屏蔽右键、拖拽、复制、文本选择等交互
- 降低长按保存图片的便利性 - 降低长按保存图片的便利性
- 给受保护页面增加动态水印覆盖
### 3. `Activities` / `Ranking` 图片受保护地址层 ### 3. `Activities` / `Ranking` 图片受保护地址层
@ -77,7 +70,7 @@
### 4. `background-image` 泄露点补丁 ### 4. `background-image` 泄露点补丁
部分重点页面之前通过内联 `background-image: url(...)` 直接暴露真实地址,这类点位已经做了兼容收口。 部分重点页面之前通过内联 `background-image: url(...)` 直接暴露真实地址,这类点位已经做了兼容收口。
已调整文件: 已调整文件:
@ -106,8 +99,6 @@
- CSS 文件名也改为哈希命名 - CSS 文件名也改为哈希命名
- 静态资源文件名同样使用哈希 - 静态资源文件名同样使用哈希
这一步不能阻止逆向,但能降低别人直接从 `dist/` 推断页面结构的便利性。
### 6. 生产包自动清理日志,并保留可切换开关 ### 6. 生产包自动清理日志,并保留可切换开关
生产相关打包模式下,会自动清掉常见调试日志;同时保留一个方便排查 App 内问题的“保留日志”构建开关。 生产相关打包模式下,会自动清掉常见调试日志;同时保留一个方便排查 App 内问题的“保留日志”构建开关。
@ -120,9 +111,9 @@
表现: 表现:
- `production` `production-atu` 下自动移除 `console.log` - `production` 下自动移除 `console.log`
- `production` `production-atu` 下自动移除 `console.debug` - `production` 下自动移除 `console.debug`
- `production` `production-atu` 下自动移除 `console.info` - `production` 下自动移除 `console.info`
- 生产相关模式下自动移除 `debugger` - 生产相关模式下自动移除 `debugger`
- `console.warn``console.error` 保留,方便线上排查 - `console.warn``console.error` 保留,方便线上排查
- Jenkins 仍可继续使用原有默认打包命令 - Jenkins 仍可继续使用原有默认打包命令
@ -132,8 +123,6 @@
```bash ```bash
npm run build:prod npm run build:prod
npm run build:prod:logs npm run build:prod:logs
npm run build:atu-prod
npm run build:atu-prod:logs
``` ```
### 7. 部分高风险页面改为延迟挂载主内容 DOM ### 7. 部分高风险页面改为延迟挂载主内容 DOM
@ -153,17 +142,9 @@ npm run build:atu-prod:logs
- 对依赖 `IntersectionObserver` 的页面,在 `await nextTick()` 之后再注册观察器 - 对依赖 `IntersectionObserver` 的页面,在 `await nextTick()` 之后再注册观察器
- 预加载结束逻辑仍然放在 `finally` 中,避免预加载失败时页面长期空白 - 预加载结束逻辑仍然放在 `finally` 中,避免预加载失败时页面长期空白
这一步的作用要明确:
- 可以减少初始阶段 DOM 直接暴露的程度
- 可以让“页面刚进来就看 Elements 面板抄结构”变得没那么直接
- 不能阻止页面渲染完成后的继续抓取
也就是说,它属于弱加固,而不是决定性的防扒手段。
### 8. 装饰背景堆图改为可选 canvas 渲染 ### 8. 装饰背景堆图改为可选 canvas 渲染
公共背景组件现在支持可选的 canvas 渲染模式,适合拿来处理活动页和榜单页那种“纯装饰背景层层堆图”的场景。 公共背景组件现在支持可选的 canvas 渲染模式,适合处理活动页和榜单页那种“纯装饰背景层层堆图”的场景。
主要文件: 主要文件:
@ -184,12 +165,10 @@ npm run build:atu-prod:logs
表现: 表现:
- 多张纯装饰背景图会先合成到单个 canvas 中,而不是在 DOM 里直接挂一 `<img>` - 多张纯装饰背景图会先合成到单个 canvas 中,而不是在 DOM 里直接挂一个个 `<img>`
- 从 Elements 面板直接看页面背景结构会比之前更不直观 - 从 Elements 面板直接看页面背景结构会比之前更不直观
- 受保护图片地址仍然继续走现有的运行时解析和缓存链路 - 受保护图片地址仍然继续走现有的运行时解析和缓存链路
这一步不能阻止有经验的人继续拿到像素内容,但能明显降低“直接看 DOM 抄背景结构”的便利度。
### 9. 生产构建混淆再收紧,并补一轮明显语义类名收口 ### 9. 生产构建混淆再收紧,并补一轮明显语义类名收口
生产相关构建模式现在显式开启更严格的 `esbuild` 压缩参数,同时针对重点页面收了一轮最显眼的本地类名。 生产相关构建模式现在显式开启更严格的 `esbuild` 压缩参数,同时针对重点页面收了一轮最显眼的本地类名。
@ -204,35 +183,17 @@ npm run build:atu-prod:logs
- [src/views/Ranking/Overall/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Overall/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) - [src/views/Ranking/WeeklyStar/WeeklyStar.vue](/D:/programs/likei-h5/src/views/Ranking/WeeklyStar/WeeklyStar.vue)
表现:
- 生产相关构建会显式压缩标识符、语法和空白
- 构建产物中的合法注释会被去掉
- 一部分过于直白的页面本地类名已经换成更短、更弱语义的名字
### 10. `Ranking/Couple` 注释修复与背景组件说明补充 ### 10. `Ranking/Couple` 注释修复与背景组件说明补充
近期对 [src/views/Ranking/Couple/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/Ranking.vue) 做了一次编码与注释清理: 近期对 [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` 是显式开关,不默认全局开启 - [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(...)` - [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)
## 这套方案能挡住什么 ## 这套方案能挡住什么
@ -247,20 +208,16 @@ npm run build:atu-prod:logs
## 当前边界 ## 当前边界
这套改动然无法彻底防住以下情况: 这套改动然无法彻底防住以下情况:
- 通过 Network 面板或抓包获取图片请求 - 通过 Network 面板或抓包获取图片请求
- 有经验的人持续分析前端运行时代码 - 有经验的人持续分析前端运行时代码
- 自定义 WebView、自动化脚本、Hook 等方式截获资源 - 自定义 WebView、自动化脚本、Hook 等方式截获资源
- 仅凭视觉观察后重新临摹布局和样式 - 仅凭视觉观察后重新临摹布局和样式
- 从一个本来就被允许渲染页面的客户端中提取 DOM、CSS、JS - 从本来就被允许渲染页面的客户端中提取 DOM、CSS、JS
一个关键原则是:
只要客户端能把页面渲染出来,客户端就一定拿到了足够还原页面的信息。 只要客户端能把页面渲染出来,客户端就一定拿到了足够还原页面的信息。
所以前端侧能做的,本质上只有“提高成本”,而不是“绝对防住”。
## 推荐测试方式 ## 推荐测试方式
### 受保护页面拦截 ### 受保护页面拦截
@ -308,17 +265,3 @@ npm run build
- 构建产物文件名中不再直观出现页面名 - 构建产物文件名中不再直观出现页面名
- 生产包中不再保留 `console.log``console.debug``console.info``debugger` - 生产包中不再保留 `console.log``console.debug``console.info``debugger`
## 后续仍可继续推进的方向
如果继续坚持只改前端项目,可以优先考虑:
- 把更敏感的装饰层、外框层继续往 `canvas` 渲染收口
- 继续减少关键活动页中可直接阅读的结构语义
- 继续替换剩余直接 `background-image` 或直出图片链路
如果后续允许少量后端配合,优先级更高的是:
- 敏感 OSS 目录改成私有读
- 使用短时签名 URL
- 给敏感图片做按用户生成的动态水印
- 资源下发前校验 App 会话或设备身份

View File

@ -7,29 +7,28 @@
- 生产构建混淆进一步收紧,`esbuild` 在生产相关模式下显式开启了更强的标识符、语法和空白压缩,并去除了合法注释输出。相关文件在 [vite.config.js](/D:/programs/likei-h5/vite.config.js)。 - 生产构建混淆进一步收紧,`esbuild` 在生产相关模式下显式开启了更强的标识符、语法和空白压缩,并去除了合法注释输出。相关文件在 [vite.config.js](/D:/programs/likei-h5/vite.config.js)。
- 公共背景组件 [BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue) 新增了 `useCanvas` 模式,装饰性多层背景会优先绘制到单个 canvas而不是在 DOM 中直接暴露多张 `<img>` - 公共背景组件 [BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue) 新增了 `useCanvas` 模式,装饰性多层背景会优先绘制到单个 canvas而不是在 DOM 中直接暴露多张 `<img>`
- `canvas` 背景已扩展到更多高风险页面: - `canvas` 背景已扩展到更多高风险页面:
[LesserBairam/index.vue](/D:/programs/likei-h5/src/views/Activities/LesserBairam/index.vue), [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), [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/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), [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/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/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/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/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/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/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), [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), [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/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), [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/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/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) 已额外做了时序加固: - [SpringFestival/Ranking.vue](/D:/programs/likei-h5/src/views/Activities/SpringFestival/Ranking.vue) 已额外做了时序加固:
组件会在 `nextTick()` 之后再注册 `IntersectionObserver`,并在 `onUnmounted` 时执行 `disconnect()`,避免监听节点还没稳定挂载时就注册,或页面切换后残留观察器。 组件会在 `nextTick()` 之后再注册 `IntersectionObserver`,并在 `onUnmounted` 时执行 `disconnect()`
- [Ranking/Couple/Ranking.vue](/D:/programs/likei-h5/src/views/Ranking/Couple/Ranking.vue) 已清理上一轮遗留的乱码注释,并统一替换为中文注释,方便后续排查与跨分支迁移。 - [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` - [BackgroundLayer.vue](/D:/programs/likei-h5/src/components/BackgroundLayer.vue) 的 `useCanvas` 继续保持显式开启,而没有改成默认值 `true`
这是为了避免把所有背景层都切到 canvas 渲染路径,给普通页面增加不必要的加载、绘制和重绘成本。 - [protectedAssets.js](/D:/programs/likei-h5/src/utils/protectedAssets.js) 中的 `toCssBackgroundImage()` 作用已明确:
- [protectedAssets.js](/D:/programs/likei-h5/src/utils/protectedAssets.js) 中的 `toCssBackgroundImage()` 作用也已明确:
它负责把 `likei-protected:` 地址还原为 CSS 可直接使用的 `url(...)` 字符串,主要用于必须保留 `background-image` 的内联样式场景。 它负责把 `likei-protected:` 地址还原为 CSS 可直接使用的 `url(...)` 字符串,主要用于必须保留 `background-image` 的内联样式场景。
## 当前边界 ## 当前边界
@ -42,17 +41,4 @@
- 使用 `npm run build` 验证生产包可正常构建。 - 使用 `npm run build` 验证生产包可正常构建。
- 使用 `npm run dev:prod``npm run preview` 验证活动页、榜单页在生产态逻辑下仍能正常显示。 - 使用 `npm run dev:prod``npm run preview` 验证活动页、榜单页在生产态逻辑下仍能正常显示。
- 重点检查 `SpringFestival``Season4``WeeklyStar``Overall` 这几类近期改动较多的页面。 - 重点检查 `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。

View File

@ -1,47 +1,24 @@
<script setup> <script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onMounted } from 'vue'
import { RouterView, useRoute } from 'vue-router' import { RouterView, useRoute } from 'vue-router'
import { useLangStore } from '@/stores/lang' import { useLangStore } from '@/stores/lang'
import { isPublicPath } from '@/config/security.js' import { isPublicPath } from '@/config/security.js'
import { getUserAccount, getUserId } from './utils/userStore.js'
import { isDebugMode } from './utils/env.js' import { isDebugMode } from './utils/env.js'
const langStore = useLangStore() const langStore = useLangStore()
const route = useRoute() const route = useRoute()
const watermarkTimestamp = ref(Date.now())
let watermarkTimer = null
const showWatermark = computed(() => !isDebugMode() && !isPublicPath(route.path))
const watermarkText = computed(() => {
const account = getUserAccount() || getUserId() || 'LIKEI'
const time = new Date(watermarkTimestamp.value).toLocaleString()
return `${account} ${time}`
})
const watermarkItems = computed(() => Array.from({ length: 12 }, (_, index) => `${index}`))
const protectedPageClass = computed(() => ({ const protectedPageClass = computed(() => ({
'protected-page': !isDebugMode() && !isPublicPath(route.path), 'protected-page': !isDebugMode() && !isPublicPath(route.path),
})) }))
onMounted(() => { onMounted(() => {
langStore.initLanguage() langStore.initLanguage()
watermarkTimer = window.setInterval(() => {
watermarkTimestamp.value = Date.now()
}, 60 * 1000)
})
onBeforeUnmount(() => {
if (watermarkTimer) {
window.clearInterval(watermarkTimer)
}
}) })
</script> </script>
<template> <template>
<div id="app" :class="protectedPageClass"> <div id="app" :class="protectedPageClass">
<div v-if="showWatermark" class="security-watermark" aria-hidden="true">
<span v-for="item in watermarkItems" :key="item">{{ watermarkText }}</span>
</div>
<RouterView /> <RouterView />
</div> </div>
</template> </template>
@ -82,31 +59,4 @@ body {
-webkit-touch-callout: none; -webkit-touch-callout: none;
user-select: 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;
}
</style> </style>