文章目录
- [一、笔记前言 & 核心安全认知](#一、笔记前言 & 核心安全认知)
- 二、页面内容防窃取核心模块(用户侧防盗)
- 三、防调试、防扒源码、防控制台篡改模块
- 四、页面水印溯源体系(核心溯源手段,无法彻底规避)
- [五、国密安全 & 接口防篡改体系](#五、国密安全 & 接口防篡改体系)
- 六、前端基础安全防护(防XSS、防劫持、防爬虫)
- [七、前端存储安全 & 动态脱敏防护](#七、前端存储安全 & 动态脱敏防护)
- 八、后端配套强制安全配置(前端无法替代)
- 九、所有防护方案漏洞汇总(真实项目踩坑)
- 10、项目目录标准化结构(可直接落地)
-
- src/protect/hooks/useCopyGuard.ts
- src/protect/hooks/useContextMenuGuard.ts
- src/protect/hooks/useImageGuard.ts
- src/protect/hooks/useDevToolsGuard.ts
- src/protect/hooks/usePrintGuard.ts
- src/protect/hooks/useXssGuard.ts
- src/protect/hooks/useFrameGuard.ts
- src/protect/hooks/useEnvDetect.ts
- src/protect/hooks/useScreenCaptureGuard.ts
- src/protect/hooks/useStorageSecure.ts
- src/protect/hooks/usePageVisibilityGuard.ts
- src/protect/hooks/useClipboardReadGuard.ts
- src/protect/hooks/useSm3Crypto.ts
- src/protect/hooks/useRequestSecure.ts
- src/protect/directives/protectContent.ts
- src/protect/components/Watermark.vue
- [src/protect/index.ts 统一导出入口](#src/protect/index.ts 统一导出入口)
- [App.vue 全局使用示例](#App.vue 全局使用示例)
- [CSS 全局打印补充(放到 global.css)](#CSS 全局打印补充(放到 global.css))
- 十一、全局统一引入使用方案
- 十二、项目落地总结(核心结论)
一、笔记前言 & 核心安全认知
1.1 核心安全定律
前端无绝对安全,只有「提高窃取成本、可溯源、防普通盗爬」」
所有前端 JS/CSS 防护均可被以下方式绕过:
- 关闭 JavaScript
- 浏览器抓包、view-source 查看源码
- 系统截图、录屏、OCR 识别
- 爬虫无头浏览器绕过检测
真正的安全 = 前端多层防护 + 后端强校验 + 资源防盗链 + 接口签名 + 行为溯源
1.2 本笔记覆盖范围
整合全网验证、企业级项目实战方案:内容防复制、防右键、防图片保存、防调试控制台、页面水印、隐形暗水印、SM3国密签名、防XSS、防iframe劫持、防爬虫、防打印、防截屏API、存储加密、接口安全、页面脱敏等全套方案。
所有功能全部组件化/Hooks化/指令化,支持 Vue3+TS / 原生 JS 项目复用。
1.3 项目防护分层架构(标准落地顺序)
- 底层服务端安全:安全响应头、防盗链、接口鉴权、防重放、SM3签名校验
- 浏览器基础安全:CSP策略、XSS防御、iframe防嵌套、权限策略
- 页面内容防窃取:禁止复制/右键/拖拽/保存图片/打印
- 环境防调试防爬:控制台冻结、无限debugger、开发者工具检测、爬虫检测
- 溯源安全:明水印、隐形暗水印、用户唯一标识溯源
- 动态脱敏安全:页面切后台脱敏、窗口失焦保护
二、页面内容防窃取核心模块(用户侧防盗)
2.1 禁止复制/剪切/粘贴(useCopyGuard)
功能:全局禁止复制,或复制自动追加版权溯源文本
适用场景:文档、合同、付费内容、涉密页面
绕过漏洞:关闭JS、查看源码手动复制
2.2 禁止右键菜单(useContextMenuGuard)
核心优化:不拦截 input/textarea/可编辑区域,不影响正常业务操作
功能:禁止右键另存为、检查元素、查看网页源代码
2.3 图片防保存/防拖拽/防长按(useImageGuard)
覆盖端:PC拖拽保存、移动端长按保存、图片另存为
实现原理:拦截 dragstart + 禁用移动端触摸菜单 + 禁止选中
2.4 全局文本禁止选中(vProtectContent 指令)
统一兼容多浏览器 user-select 样式,禁止鼠标选中页面内容,辅助防复制。
2.5 禁止页面打印(usePrintGuard)
拦截浏览器 Ctrl+P 打印行为,同时搭配 print 媒体查询,即使绕过JS也只打印水印文字。
三、防调试、防扒源码、防控制台篡改模块
3.1 控制台防护(useDevToolsGuard)
三大核心能力:
- 冻结 console 对象,防止被覆盖篡改
- 无限 debugger 阻塞控制台调试
- 窗口尺寸差值检测开发者工具是否打开
作用:极大提高前端源码调试、逆向、接口扒取成本
四、页面水印溯源体系(核心溯源手段,无法彻底规避)
4.1 显性画布水印组件(Watermark.vue)
核心特性(企业级)
- Canvas 绘制,不占用DOM节点,性能高
- MutationObserver 监听:删除水印、修改样式、隐藏DOM自动重建
- 窗口resize自动重绘
- pointer-events:none 不影响任何页面交互
- 支持自定义内容、透明度、角度、间距
价值:截图泄露可直接溯源到具体用户、账号、时间
4.2 隐形暗水印(进阶)
将用户信息、设备信息嵌入图片像素,肉眼不可见,截图/保存图片后可后台解析溯源。
局限:重度压缩、裁剪、马赛克会失效
五、国密安全 & 接口防篡改体系
5.1 SM3 国密签名(useSm3Crypto)
业务作用:
- 接口请求参数签名,防止参数篡改
- 配合 timestamp + nonce 防止接口重放攻击
- 响应数据摘要校验,保证数据完整性
安全红线:前端禁止存放核心密钥,仅做签名展示,后端全权校验
5.2 接口安全增强(useRequestSecure)
统一生成请求头:时间戳、随机串、签名、请求唯一ID,用于日志溯源、防刷、防重放。
六、前端基础安全防护(防XSS、防劫持、防爬虫)
6.1 CSP 内容安全策略
浏览器级安全策略,禁止非法脚本、eval、外部资源注入,最有效防XSS手段。优先后端响应头配置,备选 Meta 标签。
6.2 XSS 输入过滤(useXssGuard)
转义特殊字符,防止用户输入恶意脚本注入,富文本场景搭配 DOMPurify 使用。
6.3 防 iframe 嵌套点击劫持(useFrameGuard)
检测页面是否被嵌套,自动跳出顶层框架;搭配后端 X-Frame-Options 头双重防护。
6.4 爬虫/无头浏览器检测(useEnvDetect)
检测 webdriver 标记,识别自动化爬虫、模拟器访问,拦截批量爬取数据。
6.5 禁止浏览器截屏API(useScreenCaptureGuard)
拦截网页录屏、窗口捕获API,无法阻止系统截屏,仅阻断网页级窃取。
6.6 剪贴板防护(useClipboardReadGuard)
禁用网页读取剪贴板权限,防止恶意脚本窃取用户剪贴板隐私。
七、前端存储安全 & 动态脱敏防护
7.1 本地存储加密过期(useStorageSecure)
对 localStorage 数据加密、自动过期,避免明文存储用户信息、业务数据。
规范:敏感数据绝不存前端,仅非核心缓存使用。
7.2 页面切后台动态脱敏(usePageVisibilityGuard)
页面失焦/切后台自动模糊、隐藏敏感内容,切回恢复,防止后台挂机被偷窥、截图。
八、后端配套强制安全配置(前端无法替代)
所有前端防护必须搭配以下后端能力,否则形同虚设
- 图片资源防盗链:Referer 校验 + 临时签名URL + 过期时效
- Cookie 安全配置:HttpOnly、Secure、SameSite,禁止JS读取核心Cookie
- 浏览器安全响应头:
- X-Frame-Options: DENY 防嵌套劫持
- X-XSS-Protection 浏览器XSS防护
- X-Content-Type-Options: nosniff
- Referrer-Policy、Permissions-Policy
- 接口安全:SM3签名校验、时间戳窗口校验、防重放、接口权限拦截
- 访问日志溯源:记录用户、设备、IP、时间、操作行为,泄露可追责
九、所有防护方案漏洞汇总(真实项目踩坑)
- JS类防护通用漏洞:关闭JavaScript即可绕过所有复制、右键、调试拦截
- 源码泄露漏洞:view-source、抓包可查看原始代码,前端无法拦截
- 终极绕过手段:系统截图、录屏、手机拍照、OCR识别,无任何前端手段可彻底阻止
- 爬虫绕过:无头浏览器可修改webdriver标记,绕过爬虫检测
- 水印漏洞:强压缩、裁剪、马赛克可破坏暗水印,明水印只能溯源不能阻止盗取
- CSP漏洞:配置不当会导致页面样式、脚本失效,需精细化配置白名单
10、项目目录标准化结构(可直接落地)
src/protect
├── hooks/
│ ├── useCopyGuard.ts // 复制防护
│ ├── useContextMenuGuard.ts // 右键防护
│ ├── useImageGuard.ts // 图片防盗
│ ├── useDevToolsGuard.ts // 控制台防调试
│ ├── usePrintGuard.ts // 禁止打印
│ ├── useXssGuard.ts // XSS过滤
│ ├── useFrameGuard.ts // 防iframe嵌套
│ ├── useEnvDetect.ts // 爬虫检测
│ ├── useScreenCaptureGuard.ts // 截屏API拦截
│ ├── useStorageSecure.ts // 存储加密
│ ├── usePageVisibilityGuard.ts // 切页脱敏
│ ├── useClipboardReadGuard.ts // 剪贴板防护
│ ├── useSm3Crypto.ts // SM3国密签名
│ └── useRequestSecure.ts // 接口安全
├── directives/
│ └── protectContent.ts // 禁止选中指令
├── components/
│ └── Watermark.vue // 防篡改水印组件
└── index.ts // 统一出口
目录严格按照笔记
src/protect,Vue3 + TypeScript,Hooks、指令、组件完整源码,可直接复制到项目使用⚠️注意:
sm‑crypto需要安装依赖:npm install sm‑crypto
src/protect/hooks/useCopyGuard.ts
ts
/**
* 复制剪切防护:禁止复制 / 复制追加版权文本
*/
type CopyMode = 'disable' | 'append'
export function useCopyGuard(mode: CopyMode = 'disable', copyrightText = "\n内容版权所有,禁止私自转载") {
const handleCopy = (e: ClipboardEvent) => {
if (mode === 'disable') {
e.preventDefault()
return
}
const selText = window.getSelection()?.toString() ?? ''
e.clipboardData?.setData('text/plain', selText + copyrightText)
e.preventDefault()
}
const handleCut = (e: ClipboardEvent) => {
e.preventDefault()
}
const bind = () => {
document.addEventListener('copy', handleCopy)
document.addEventListener('cut', handleCut)
}
const unbind = () => {
document.removeEventListener('copy', handleCopy)
document.removeEventListener('cut', handleCut)
}
return { bind, unbind }
}
src/protect/hooks/useContextMenuGuard.ts
ts
/**
* 右键菜单防护,放过 input/textarea/contenteditable
*/
export function useContextMenuGuard() {
const handler = (e: MouseEvent) => {
const target = e.target as HTMLElement
const tag = target.tagName
if (['INPUT', 'TEXTAREA'].includes(tag) || target.isContentEditable) {
return
}
e.preventDefault()
}
const bind = () => document.addEventListener('contextmenu', handler)
const unbind = () => document.removeEventListener('contextmenu', handler)
return { bind, unbind }
}
src/protect/hooks/useImageGuard.ts
ts
/**
* 图片防拖拽、移动端长按保存
*/
export function useImageGuard() {
const dragHandler = (e: DragEvent) => e.preventDefault()
const bind = () => {
document.querySelectorAll<HTMLImageElement>('img').forEach(img => {
img.addEventListener('dragstart', dragHandler)
img.style.webkitTouchCallout = 'none'
img.style.userSelect = 'none'
})
}
const unbind = () => {
document.querySelectorAll<HTMLImageElement>('img').forEach(img => {
img.removeEventListener('dragstart', dragHandler)
})
}
return { bind, unbind }
}
src/protect/hooks/useDevToolsGuard.ts
ts
/**
* 开发者工具防护:冻结console、无限debugger、检测控制台打开
*/
export function useDevToolsGuard(onDetect?: () => void) {
let debugTimer: number | null = null
let detectTimer: number | null = null
// 冻结console
const freezeConsole = () => {
try {
Object.freeze(console)
} catch {}
}
// 无限debugger
const infiniteDebugger = () => {
const fn = () => {
try { debugger } catch {}
}
debugTimer = window.setInterval(fn, 400)
}
// 检测devtools窗口尺寸
const detect = () => {
const threshold = 150
const wDiff = window.outerWidth - window.innerWidth
const hDiff = window.outerHeight - window.innerHeight
if (wDiff > threshold || hDiff > threshold) {
onDetect?.()
}
}
const start = () => {
freezeConsole()
infiniteDebugger()
detectTimer = window.setInterval(detect, 700)
}
const stop = () => {
if (debugTimer) clearInterval(debugTimer)
if (detectTimer) clearInterval(detectTimer)
}
return { start, stop }
}
src/protect/hooks/usePrintGuard.ts
/**
* 拦截浏览器打印 Ctrl+P
*/
export function usePrintGuard() {
const beforePrintHandler = (e: Event) => {
e.preventDefault()
}
const bind = () => window.addEventListener('beforeprint', beforePrintHandler)
const unbind = () => window.removeEventListener('beforeprint', beforePrintHandler)
return { bind, unbind }
}
src/protect/hooks/useXssGuard.ts
ts
/**
* XSS简单转义,富文本建议使用DOMPurify
*/
export function useXssGuard() {
const escapeHtml = (str: string): string => {
if (!str) return ''
const map: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}
return str.replace(/[&<>"']/g, m => map[m])
}
return { escapeHtml }
}
src/protect/hooks/useFrameGuard.ts
ts
/**
* 防iframe嵌套点击劫持
*/
export function useFrameGuard() {
const check = () => {
if (window.top !== window.self) {
window.top.location.replace(window.location.href)
}
}
return { check }
}
src/protect/hooks/useEnvDetect.ts
ts
/**
* 简易爬虫/无头浏览器检测
*/
export function useEnvDetect(onDetect: () => void) {
const checkRobot = () => {
if ((navigator as any).webdriver) {
onDetect()
return true
}
return false
}
return { checkRobot }
}
src/protect/hooks/useScreenCaptureGuard.ts
ts
/**
* 拦截网页录屏API,不能阻止系统截图
*/
export function useScreenCaptureGuard() {
const blockCapture = () => {
if (!navigator.mediaDevices?.getDisplayMedia) return
const original = navigator.mediaDevices.getDisplayMedia
navigator.mediaDevices.getDisplayMedia = async (...args) => {
throw new Error('屏幕捕获已禁用')
return await original.apply(navigator.mediaDevices, args)
}
}
return { blockCapture }
}
src/protect/hooks/useStorageSecure.ts
ts
/**
* localStorage简单base64封装,带过期;禁止存真正敏感密码密钥
*/
export function useStorageSecure() {
const setEncryptItem = (key: string, value: string, expireMs: number) => {
const payload = { value, expire: Date.now() + expireMs }
localStorage.setItem(key, btoa(JSON.stringify(payload)))
}
const getItem = (key: string): string | null => {
const raw = localStorage.getItem(key)
if (!raw) return null
try {
const data = JSON.parse(atob(raw))
if (Date.now() > data.expire) {
localStorage.removeItem(key)
return null
}
return data.value
} catch {
return null
}
}
return { setEncryptItem, getItem }
}
src/protect/hooks/usePageVisibilityGuard.ts
ts
/**
* 页面切后台/失焦监听,用于敏感内容自动脱敏
*/
export function usePageVisibilityGuard(onHide: () => void, onShow: () => void) {
const handler = () => {
if (document.hidden) {
onHide()
} else {
onShow()
}
}
const bind = () => document.addEventListener('visibilitychange', handler)
const unbind = () => document.removeEventListener('visibilitychange', handler)
return { bind, unbind }
}
src/protect/hooks/useClipboardReadGuard.ts
ts
/**
* 阻止网页JS读取剪贴板
*/
export function useClipboardReadGuard() {
const bind = () => {
if (!navigator.clipboard) return
;(navigator.clipboard as any).read = () => Promise.reject(new Error('剪贴板读取被禁止'))
;(navigator.clipboard as any).readText = () => Promise.reject(new Error('剪贴板读取被禁止'))
}
return { bind }
}
src/protect/hooks/useSm3Crypto.ts
ts
/**
* SM3国密摘要与请求签名工具
* 重要:密钥secret禁止前端硬编码
*/
import { sm3 } from 'sm‑crypto'
export function useSm3Crypto() {
const getSm3Hash = (plainText: string): string => {
return sm3(plainText)
}
const createSign = (params: Record<string, any>, secret: string) => {
const timestamp = Date.now().toString()
const nonce = Math.random().toString(36).slice(2)
const rawStr = Object.values(params).sort().join('') + timestamp + nonce + secret
const sign = sm3(rawStr)
return { sign, timestamp, nonce }
}
return { getSm3Hash, createSign }
}
src/protect/hooks/useRequestSecure.ts
ts
/**
* 请求安全头封装,配合SM3签名
*/
import { useSm3Crypto } from './useSm3Crypto'
export function useRequestSecure() {
const { createSign } = useSm3Crypto()
const getSecurityHeaders = (params: Record<string, any>, secret: string) => {
const { sign, timestamp, nonce } = createSign(params, secret)
return {
'X‑Timestamp': timestamp,
'X‑Nonce': nonce,
'X‑Sign': sign,
'X‑Request‑Id': crypto.randomUUID()
}
}
return { getSecurityHeaders }
}
src/protect/directives/protectContent.ts
ts
/**
* v‑protect‑content 禁止文本选中指令
*/
import type { Directive } from 'vue'
export const vProtectContent: Directive = {
mounted(el) {
el.style.userSelect = 'none'
el.style.webkitUserSelect = 'none'
el.style.msUserSelect = 'none'
el.style.MozUserSelect = 'none'
}
}
src/protect/components/Watermark.vue
html
<template>
<div ref="watermarkRef" class="watermark-wrap"></div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
interface WatermarkOption {
content: string
fontSize?: number
color?: string
rotate?: number
gap?: number
}
const props = defineProps<{
option: WatermarkOption
}>()
const watermarkRef = ref<HTMLDivElement | null>(null)
let observer: MutationObserver | null = null
let resizeFn: () => void
function generateBg(opt: WatermarkOption): string {
const canvas = document.createElement('canvas')
const { content, fontSize = 14, color = 'rgba(180,180,180,0.12)', rotate = -25, gap = 130 } = opt
canvas.width = gap
canvas.height = gap
const ctx = canvas.getContext('2d')!
ctx.rotate((rotate * Math.PI) / 180)
ctx.font = `${fontSize}px sans‑serif`
ctx.fillStyle = color
ctx.fillText(content, 12, gap / 2)
return canvas.toDataURL('image/png')
}
function render() {
if (!watermarkRef.value) return
const bgUrl = generateBg(props.option)
watermarkRef.value.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z‑index: 9999;
pointer‑events: none;
background‑repeat: repeat;
background‑image: url(${bgUrl});
`
}
onMounted(() => {
render()
observer = new MutationObserver(() => render())
observer.observe(watermarkRef.value!, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['style', 'class']
})
resizeFn = () => render()
window.addEventListener('resize', resizeFn)
})
onUnmounted(() => {
observer?.disconnect()
window.removeEventListener('resize', resizeFn)
})
watch(() => props.option, () => render(), { deep: true })
</script>
<style scoped>
.watermark-wrap {
overflow: hidden;
}
</style>
src/protect/index.ts 统一导出入口
ts
// hooks
export { useCopyGuard } from './hooks/useCopyGuard'
export { useContextMenuGuard } from './hooks/useContextMenuGuard'
export { useImageGuard } from './hooks/useImageGuard'
export { useDevToolsGuard } from './hooks/useDevToolsGuard'
export { usePrintGuard } from './hooks/usePrintGuard'
export { useXssGuard } from './hooks/useXssGuard'
export { useFrameGuard } from './hooks/useFrameGuard'
export { useEnvDetect } from './hooks/useEnvDetect'
export { useScreenCaptureGuard } from './hooks/useScreenCaptureGuard'
export { useStorageSecure } from './hooks/useStorageSecure'
export { usePageVisibilityGuard } from './hooks/usePageVisibilityGuard'
export { useClipboardReadGuard } from './hooks/useClipboardReadGuard'
export { useSm3Crypto } from './hooks/useSm3Crypto'
export { useRequestSecure } from './hooks/useRequestSecure'
// directives
export { vProtectContent } from './directives/protectContent'
// components
export { default as Watermark } from './components/Watermark.vue'
App.vue 全局使用示例
html
<script setup lang="ts">
import {
useCopyGuard,
useContextMenuGuard,
useImageGuard,
useDevToolsGuard,
usePrintGuard,
useFrameGuard,
Watermark
} from '@/protect'
// 复制防护
const { bind: bindCopy } = useCopyGuard('disable')
// 右键防护
const { bind: bindRight } = useContextMenuGuard()
// 图片防盗
const { bind: bindImg } = useImageGuard()
// 打印防护
const { bind: bindPrint } = usePrintGuard()
// 防iframe嵌套
const { check: frameCheck } = useFrameGuard()
// 控制台防护
const { start: startDevProtect } = useDevToolsGuard(() => {
console.warn('检测到开发者工具打开')
})
bindCopy()
bindRight()
bindImg()
bindPrint()
frameCheck()
startDevProtect()
</script>
<template>
<Watermark :option="{ content: '内部文档|禁止外泄|userId‑001' }"/>
<router-view />
</template>
CSS 全局打印补充(放到 global.css)
css
@media print {
body * {
visibility: hidden !important;
}
}
十一、全局统一引入使用方案
在 App.vue 全局一次性注册所有防护,全局生效,无需逐个页面引入。
敏感页面可单独开启动态脱敏、高阶水印强化防护。
十二、项目落地总结(核心结论)
- 前端防护的本质:防小白、防批量爬取、提高盗取成本、留存溯源证据
- 绝对安全依赖:后端校验 + 资源权限 + 签名防篡改 + 日志溯源
- 企业级标准方案:内容防护 + 环境防护 + 数据安全 + 溯源追责四层闭环
- 所有功能均已组件化,支持按需引入、按需关闭,不影响项目性能与业务