chore: 取消动态水印
This commit is contained in:
parent
816f4a1842
commit
073c61e6bb
@ -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`
|
||||
- 按手册把高风险页面优先迁移,并同步更新文档
|
||||
|
||||
看到这类指令后,我默认会:
|
||||
|
||||
- 先核对分支当前状态
|
||||
- 再按这份文档逐项执行
|
||||
- 遇到结构差异时在不偏离目标的前提下做适配
|
||||
- 最后把实际执行结果写回文档
|
||||
@ -2,20 +2,20 @@
|
||||
|
||||
## 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.
|
||||
|
||||
## 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
|
||||
- private OSS buckets
|
||||
- signed image URLs
|
||||
- server-side watermark rendering
|
||||
- server-side image rewriting
|
||||
|
||||
## What Was Added
|
||||
|
||||
@ -37,7 +37,7 @@ Behavior:
|
||||
|
||||
### 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:
|
||||
|
||||
@ -50,7 +50,6 @@ 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`
|
||||
|
||||
@ -69,7 +68,7 @@ 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`
|
||||
- pages that missed `v-smart-img` still get a runtime fallback for protected image `src`
|
||||
|
||||
### 4. `background-image` compatibility fixes
|
||||
|
||||
@ -102,11 +101,9 @@ Behavior:
|
||||
- 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.
|
||||
Production-like builds strip common debug output while still supporting a keep-logs build for app-side troubleshooting.
|
||||
|
||||
Main files:
|
||||
|
||||
@ -116,9 +113,9 @@ Main files:
|
||||
|
||||
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` builds remove `console.log`
|
||||
- `production` builds remove `console.debug`
|
||||
- `production` 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
|
||||
@ -128,8 +125,6 @@ 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
|
||||
@ -148,15 +143,10 @@ 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.
|
||||
The shared background layer component supports a canvas rendering mode for stacked background images.
|
||||
|
||||
Main file:
|
||||
|
||||
@ -181,11 +171,9 @@ Behavior:
|
||||
- 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.
|
||||
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:
|
||||
|
||||
@ -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/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:
|
||||
|
||||
- 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
|
||||
- garbled comments in `Ranking/Couple/Ranking.vue` were cleaned up and unified into Chinese comments
|
||||
- `useCanvas` remains an explicit opt-in instead of becoming a global default
|
||||
- `toCssBackgroundImage()` stays as the CSS bridge for protected asset URLs
|
||||
|
||||
## What This Helps Against
|
||||
|
||||
@ -226,11 +220,7 @@ These changes still do not fully prevent:
|
||||
- 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.
|
||||
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.
|
||||
|
||||
## Recommended Testing
|
||||
|
||||
@ -249,11 +239,6 @@ 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:
|
||||
@ -279,17 +264,3 @@ Expected:
|
||||
- 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
|
||||
|
||||
@ -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
|
||||
- 服务端动态水印
|
||||
- 服务端动态出图
|
||||
|
||||
## 已落地能力
|
||||
|
||||
@ -39,7 +33,7 @@
|
||||
- 受保护页面在非 App 环境下会跳转到 `/#/not_app`
|
||||
- 受保护页面内的接口请求在缺少 App bridge 时会被拒绝
|
||||
|
||||
### 2. 运行时防调试、防复制与页面水印
|
||||
### 2. 运行时防调试与防复制限制
|
||||
|
||||
对受保护页面增加了一层轻量运行时限制。
|
||||
|
||||
@ -54,7 +48,6 @@
|
||||
- 屏蔽常见开发者工具快捷键
|
||||
- 屏蔽右键、拖拽、复制、文本选择等交互
|
||||
- 降低长按保存图片的便利性
|
||||
- 给受保护页面增加动态水印覆盖
|
||||
|
||||
### 3. `Activities` / `Ranking` 图片受保护地址层
|
||||
|
||||
@ -77,7 +70,7 @@
|
||||
|
||||
### 4. `background-image` 泄露点补丁
|
||||
|
||||
部分重点页面之前仍通过内联 `background-image: url(...)` 直接暴露真实地址,这类点位已经做了兼容收口。
|
||||
部分重点页面之前通过内联 `background-image: url(...)` 直接暴露真实地址,这类点位已经做了兼容收口。
|
||||
|
||||
已调整文件:
|
||||
|
||||
@ -106,8 +99,6 @@
|
||||
- CSS 文件名也改为哈希命名
|
||||
- 静态资源文件名同样使用哈希
|
||||
|
||||
这一步不能阻止逆向,但能降低别人直接从 `dist/` 推断页面结构的便利性。
|
||||
|
||||
### 6. 生产包自动清理日志,并保留可切换开关
|
||||
|
||||
生产相关打包模式下,会自动清掉常见调试日志;同时保留一个方便排查 App 内问题的“保留日志”构建开关。
|
||||
@ -120,9 +111,9 @@
|
||||
|
||||
表现:
|
||||
|
||||
- `production` 和 `production-atu` 下自动移除 `console.log`
|
||||
- `production` 和 `production-atu` 下自动移除 `console.debug`
|
||||
- `production` 和 `production-atu` 下自动移除 `console.info`
|
||||
- `production` 下自动移除 `console.log`
|
||||
- `production` 下自动移除 `console.debug`
|
||||
- `production` 下自动移除 `console.info`
|
||||
- 生产相关模式下自动移除 `debugger`
|
||||
- `console.warn` 和 `console.error` 保留,方便线上排查
|
||||
- Jenkins 仍可继续使用原有默认打包命令
|
||||
@ -132,8 +123,6 @@
|
||||
```bash
|
||||
npm run build:prod
|
||||
npm run build:prod:logs
|
||||
npm run build:atu-prod
|
||||
npm run build:atu-prod:logs
|
||||
```
|
||||
|
||||
### 7. 部分高风险页面改为延迟挂载主内容 DOM
|
||||
@ -153,17 +142,9 @@ npm run build:atu-prod:logs
|
||||
- 对依赖 `IntersectionObserver` 的页面,在 `await nextTick()` 之后再注册观察器
|
||||
- 预加载结束逻辑仍然放在 `finally` 中,避免预加载失败时页面长期空白
|
||||
|
||||
这一步的作用要明确:
|
||||
|
||||
- 可以减少初始阶段 DOM 直接暴露的程度
|
||||
- 可以让“页面刚进来就看 Elements 面板抄结构”变得没那么直接
|
||||
- 不能阻止页面渲染完成后的继续抓取
|
||||
|
||||
也就是说,它属于弱加固,而不是决定性的防扒手段。
|
||||
|
||||
### 8. 装饰背景堆图改为可选 canvas 渲染
|
||||
|
||||
公共背景组件现在支持可选的 canvas 渲染模式,适合拿来处理活动页和榜单页那种“纯装饰背景层层堆图”的场景。
|
||||
公共背景组件现在支持可选的 canvas 渲染模式,适合处理活动页和榜单页那种“纯装饰背景层层堆图”的场景。
|
||||
|
||||
主要文件:
|
||||
|
||||
@ -184,12 +165,10 @@ npm run build:atu-prod:logs
|
||||
|
||||
表现:
|
||||
|
||||
- 多张纯装饰背景图会先合成到单个 canvas 中,而不是在 DOM 里直接挂一串 `<img>`
|
||||
- 多张纯装饰背景图会先合成到单个 canvas 中,而不是在 DOM 里直接挂一个个 `<img>`
|
||||
- 从 Elements 面板直接看页面背景结构会比之前更不直观
|
||||
- 受保护图片地址仍然继续走现有的运行时解析和缓存链路
|
||||
|
||||
这一步不能阻止有经验的人继续拿到像素内容,但能明显降低“直接看 DOM 抄背景结构”的便利度。
|
||||
|
||||
### 9. 生产构建混淆再收紧,并补一轮明显语义类名收口
|
||||
|
||||
生产相关构建模式现在显式开启更严格的 `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/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)
|
||||
|
||||
## 这套方案能挡住什么
|
||||
|
||||
@ -247,20 +208,16 @@ npm run build:atu-prod:logs
|
||||
|
||||
## 当前边界
|
||||
|
||||
这套改动仍然无法彻底防住以下情况:
|
||||
这套改动依然无法彻底防住以下情况:
|
||||
|
||||
- 通过 Network 面板或抓包获取图片请求
|
||||
- 有经验的人持续分析前端运行时代码
|
||||
- 自定义 WebView、自动化脚本、Hook 等方式截获资源
|
||||
- 仅凭视觉观察后重新临摹布局和样式
|
||||
- 从一个本来就被允许渲染页面的客户端中提取 DOM、CSS、JS
|
||||
|
||||
一个关键原则是:
|
||||
- 从本来就被允许渲染页面的客户端中提取 DOM、CSS、JS
|
||||
|
||||
只要客户端能把页面渲染出来,客户端就一定拿到了足够还原页面的信息。
|
||||
|
||||
所以前端侧能做的,本质上只有“提高成本”,而不是“绝对防住”。
|
||||
|
||||
## 推荐测试方式
|
||||
|
||||
### 受保护页面拦截
|
||||
@ -308,17 +265,3 @@ npm run build
|
||||
- 构建产物文件名中不再直观出现页面名
|
||||
- 生产包中不再保留 `console.log`、`console.debug`、`console.info` 和 `debugger`
|
||||
|
||||
## 后续仍可继续推进的方向
|
||||
|
||||
如果继续坚持只改前端项目,可以优先考虑:
|
||||
|
||||
- 把更敏感的装饰层、外框层继续往 `canvas` 渲染收口
|
||||
- 继续减少关键活动页中可直接阅读的结构语义
|
||||
- 继续替换剩余直接 `background-image` 或直出图片链路
|
||||
|
||||
如果后续允许少量后端配合,优先级更高的是:
|
||||
|
||||
- 敏感 OSS 目录改成私有读
|
||||
- 使用短时签名 URL
|
||||
- 给敏感图片做按用户生成的动态水印
|
||||
- 资源下发前校验 App 会话或设备身份
|
||||
|
||||
@ -7,29 +7,28 @@
|
||||
- 生产构建混淆进一步收紧,`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>`。
|
||||
- `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),
|
||||
[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/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()` 作用也已明确:
|
||||
组件会在 `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`。
|
||||
- [protectedAssets.js](/D:/programs/likei-h5/src/utils/protectedAssets.js) 中的 `toCssBackgroundImage()` 作用已明确:
|
||||
它负责把 `likei-protected:` 地址还原为 CSS 可直接使用的 `url(...)` 字符串,主要用于必须保留 `background-image` 的内联样式场景。
|
||||
|
||||
## 当前边界
|
||||
@ -42,17 +41,4 @@
|
||||
|
||||
- 使用 `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。
|
||||
- 重点检查 `SpringFestival`、`Season4`、`WeeklyStar`、`Overall` 这些近期改动较多的页面。
|
||||
|
||||
52
src/App.vue
52
src/App.vue
@ -1,47 +1,24 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { RouterView, useRoute } from 'vue-router'
|
||||
import { useLangStore } from '@/stores/lang'
|
||||
import { isPublicPath } from '@/config/security.js'
|
||||
import { getUserAccount, getUserId } from './utils/userStore.js'
|
||||
import { isDebugMode } from './utils/env.js'
|
||||
|
||||
const langStore = useLangStore()
|
||||
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(() => ({
|
||||
'protected-page': !isDebugMode() && !isPublicPath(route.path),
|
||||
}))
|
||||
|
||||
onMounted(() => {
|
||||
langStore.initLanguage()
|
||||
watermarkTimer = window.setInterval(() => {
|
||||
watermarkTimestamp.value = Date.now()
|
||||
}, 60 * 1000)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (watermarkTimer) {
|
||||
window.clearInterval(watermarkTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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 />
|
||||
</div>
|
||||
</template>
|
||||
@ -82,31 +59,4 @@ body {
|
||||
-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;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user