14 KiB
前端加固说明
目的
这份文档用于记录当前仓库内已经落地的前端侧防扒取、防复用加固方案。
核心目标是提高扒图、抄页面、低成本复刻的门槛,而不是宣称“前端已经绝对防住”。
如需在后续切换到 atu-prod 分支后复用整套处理,请直接参考:
范围
本次加固严格限制在当前前端项目内完成。
当前方案不依赖:
- 后端接口改造
- OSS 私有读
- 短时签名 URL
- 服务端动态水印
已落地能力
1. 受保护页面访问限制
生产相关模式下,受保护页面要求运行在官方 App 环境中。
主要文件:
表现:
- 公共页面仍可正常访问
- 受保护页面在非 App 环境下会跳转到
/#/not_app - 受保护页面内的接口请求在缺少 App bridge 时会被拒绝
2. 运行时防调试、防复制与页面水印
对受保护页面增加了一层轻量运行时限制。
主要文件:
表现:
- 屏蔽常见开发者工具快捷键
- 屏蔽右键、拖拽、复制、文本选择等交互
- 降低长按保存图片的便利性
- 给受保护页面增加动态水印覆盖
3. Activities / Ranking 图片受保护地址层
Activities/ 和 Ranking/ 目录下的大部分图片,不再直接以真实 OSS 地址出现在模板中。
主要文件:
- src/config/imagePaths.js
- src/utils/protectedAssets.js
- src/utils/protectedAssetRuntime.js
- src/directives/smartImage.js
- 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
- src/views/Activities/LuckyDollars/Season3/index.vue
- src/views/Activities/SpringFestival/index.vue
- src/views/Activities/SpringFestival/Task.vue
- src/views/Ranking/KingAndQueen/TopList.vue
表现:
- 写入 CSS 前先通过受保护地址辅助方法解析
- 模板和样式字符串里直接出现真实图链的情况更少
5. 构建产物可读性降低
构建输出文件名从可读页面名改成了哈希命名。
主要文件:
表现:
- JS chunk 文件名不再直接暴露
TopList、WeeklyStar、Ranking等页面名 - CSS 文件名也改为哈希命名
- 静态资源文件名同样使用哈希
这一步不能阻止逆向,但能降低别人直接从 dist/ 推断页面结构的便利性。
6. 生产包自动清理日志,并保留可切换开关
生产相关打包模式下,会自动清掉常见调试日志;同时保留一个方便排查 App 内问题的“保留日志”构建开关。
主要文件:
表现:
production和production-atu下自动移除console.logproduction和production-atu下自动移除console.debugproduction和production-atu下自动移除console.info- 生产相关模式下自动移除
debugger console.warn和console.error保留,方便线上排查- Jenkins 仍可继续使用原有默认打包命令
常用命令:
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
- src/views/Activities/LuckyDollars/Season4/index.vue
- src/views/Activities/SpringFestival/index.vue
- src/views/Ranking/Couple/index.vue
- src/views/Ranking/Overall/Ranking.vue
同步补了两类兜底:
- 对依赖
IntersectionObserver的页面,在await nextTick()之后再注册观察器 - 预加载结束逻辑仍然放在
finally中,避免预加载失败时页面长期空白
这一步的作用要明确:
- 可以减少初始阶段 DOM 直接暴露的程度
- 可以让“页面刚进来就看 Elements 面板抄结构”变得没那么直接
- 不能阻止页面渲染完成后的继续抓取
也就是说,它属于弱加固,而不是决定性的防扒手段。
8. 装饰背景堆图改为可选 canvas 渲染
公共背景组件现在支持可选的 canvas 渲染模式,适合拿来处理活动页和榜单页那种“纯装饰背景层层堆图”的场景。
主要文件:
当前已接入的高风险页面:
- src/views/Activities/LesserBairam/index.vue
- src/views/Activities/LoginReward/index.vue
- src/views/Activities/LuckyDollars/Season3/index.vue
- src/views/Ranking/Couple/index.vue
- src/views/Activities/LuckyDollars/Season4/index.vue
- src/views/Activities/SpringFestival/index.vue
- src/views/Ranking/GamesKing/index.vue
- src/views/Ranking/KingAndQueen/TopList.vue
- src/views/Ranking/Overall/Ranking.vue
- src/views/Ranking/WeeklyStar/WeeklyStar.vue
表现:
- 多张纯装饰背景图会先合成到单个 canvas 中,而不是在 DOM 里直接挂一串
<img> - 从 Elements 面板直接看页面背景结构会比之前更不直观
- 受保护图片地址仍然继续走现有的运行时解析和缓存链路
这一步不能阻止有经验的人继续拿到像素内容,但能明显降低“直接看 DOM 抄背景结构”的便利度。
9. 生产构建混淆再收紧,并补一轮明显语义类名收口
生产相关构建模式现在显式开启更严格的 esbuild 压缩参数,同时针对重点页面收了一轮最显眼的本地类名。
主要文件:
- vite.config.js
- src/views/Ranking/Couple/index.vue
- src/views/Activities/LuckyDollars/Season4/index.vue
- src/views/Activities/SpringFestival/index.vue
- src/views/Activities/SpringFestival/Ranking.vue
- src/views/Ranking/Overall/Ranking.vue
- src/views/Ranking/WeeklyStar/WeeklyStar.vue
表现:
- 生产相关构建会显式压缩标识符、语法和空白
- 构建产物中的合法注释会被去掉
- 一部分过于直白的页面本地类名已经换成更短、更弱语义的名字
10. Ranking/Couple 注释修复与背景组件说明补充
近期对 src/views/Ranking/Couple/Ranking.vue 做了一次编码与注释清理:
- 清除了上一轮编辑中遗留的乱码注释
- 将核心业务注释统一收口为中文,便于后续维护和分支迁移
- 保留当前逻辑不变,只修正文案可读性与排查体验
同时补充说明两个容易混淆的背景层工具点:
- src/components/BackgroundLayer.vue 的
useCanvas是显式开关,不默认全局开启 - 原因是
BackgroundLayer是公共组件,默认全开会把所有背景层都切到 canvas 路径,带来额外的图片加载、重绘和兼容风险 - 当前只在
Activities / Ranking这类高风险页面显式传入:useCanvas=\"true\",把成本控制在需要加固的范围内 - src/utils/protectedAssets.js 中的
toCssBackgroundImage()用于把likei-protected:地址还原成 CSS 可用的background-image: url(...) - 这个方法是兼容桥接,不是额外的加密层,主要服务于仍需使用内联背景图的场景
如果后续需要在 atu-prod 分支做同类处理,建议优先对照这几个文件迁移:
- src/views/Ranking/Couple/Ranking.vue
- src/components/BackgroundLayer.vue
- src/utils/protectedAssets.js
- docs/security/frontend-hardening.zh-CN.md
- docs/security/frontend-hardening.zh-CN.updates.md
这套方案能挡住什么
当前前端加固,对以下场景有一定抬门槛作用:
- 普通浏览器直接打开受保护页面
- 低门槛 DOM 抓取
- 从模板或页面源码里直接抄图片地址
- 从可读的构建产物文件名里反推页面用途
- 简单右键、拖拽、长按保存
- 依赖“页面初始 DOM 直接可见”的低成本抄结构方式
当前边界
这套改动仍然无法彻底防住以下情况:
- 通过 Network 面板或抓包获取图片请求
- 有经验的人持续分析前端运行时代码
- 自定义 WebView、自动化脚本、Hook 等方式截获资源
- 仅凭视觉观察后重新临摹布局和样式
- 从一个本来就被允许渲染页面的客户端中提取 DOM、CSS、JS
一个关键原则是:
只要客户端能把页面渲染出来,客户端就一定拿到了足够还原页面的信息。
所以前端侧能做的,本质上只有“提高成本”,而不是“绝对防住”。
推荐测试方式
受保护页面拦截
可使用:
npm run dev:prod
或:
npm run build
npm run preview
预期结果:
- 非 App 环境下访问受保护页面会跳转到
/#/not_app - 公共页面仍可正常访问
页面与图片链路验证
在接近生产环境的模式下检查:
Activities/Ranking页面是否仍能正常展示- 受保护图片是否仍能正常显示
- 运行时 DOM 和构建产物里是否更少直接暴露真实图链
- 本次从
v-show切到v-if的页面,是否仍能在预加载结束后正常渲染 - 依赖触底加载的页面,滚动加载是否仍然正常
构建产物验证
执行:
npm run build
预期结果:
dist/js文件名以哈希为主dist/css文件名以哈希为主- 构建产物文件名中不再直观出现页面名
- 生产包中不再保留
console.log、console.debug、console.info和debugger
后续仍可继续推进的方向
如果继续坚持只改前端项目,可以优先考虑:
- 把更敏感的装饰层、外框层继续往
canvas渲染收口 - 继续减少关键活动页中可直接阅读的结构语义
- 继续替换剩余直接
background-image或直出图片链路
如果后续允许少量后端配合,优先级更高的是:
- 敏感 OSS 目录改成私有读
- 使用短时签名 URL
- 给敏感图片做按用户生成的动态水印
- 资源下发前校验 App 会话或设备身份