直接把js和html保存到本地,在浏览器打开即可看到效果
sdk.js
javascript
/**
* 水印SDK - 用于在网页上添加水印效果
* @version 2.0.0
* @author S.M.D
* 增强版 - 具备强化的防删除保护机制
*/
;(function (global) {
'use strict'
// 默认配置
const DEFAULT_CONFIG = {
text: '', // 水印文本(必填)
fontSize: 16, // 字体大小
fontColor: 'rgba(0, 0, 0, 0.15)', // 字体颜色
rotate: -20, // 旋转角度(度)
gap: [100, 100], // 水印间距 [x, y]
offset: [0, 0], // 偏移量 [x, y]
}
// 水印实例存储
let watermarkInstance = null
// 防护相关变量
let observer = null
let intervalChecker = null
let protectionEnabled = true
let originalStyles = null
let backupElements = []
/**
* 创建水印
* @param {Object} options - 水印配置选项
* @param {string} options.text - 水印文本(必填)
* @param {number} [options.fontSize=16] - 字体大小
* @param {string} [options.fontColor='rgba(0, 0, 0, 0.15)'] - 字体颜色
* @param {number} [options.rotate=-20] - 旋转角度
* @param {Array} [options.gap=[100, 100]] - 水印间距
* @param {Array} [options.offset=[0, 0]] - 偏移量
* @returns {Object} 水印实例对象
*/
function createWatermark(options = {}) {
// 验证必填参数
if (!options.text || typeof options.text !== 'string') {
throw new Error('水印文本(text)是必填参数,且必须是字符串类型')
}
// 合并配置
const config = Object.assign({}, DEFAULT_CONFIG, options)
// 验证和处理配置参数
config.fontSize = Math.max(
1,
Number(config.fontSize) || DEFAULT_CONFIG.fontSize
)
config.rotate = Number(config.rotate) || DEFAULT_CONFIG.rotate
// 处理gap参数
if (Array.isArray(config.gap)) {
config.gap = [
Math.max(1, Number(config.gap[0]) || DEFAULT_CONFIG.gap[0]),
Math.max(1, Number(config.gap[1]) || DEFAULT_CONFIG.gap[1]),
]
} else {
config.gap = DEFAULT_CONFIG.gap
}
// 处理offset参数
if (Array.isArray(config.offset)) {
config.offset = [
Number(config.offset[0]) || DEFAULT_CONFIG.offset[0],
Number(config.offset[1]) || DEFAULT_CONFIG.offset[1],
]
} else {
config.offset = DEFAULT_CONFIG.offset
}
// 移除已存在的水印
removeWatermark()
// 创建主水印元素
const watermarkEl = createWatermarkElement(config)
// 创建备用水印元素(多重保护)
const backupEl1 = createWatermarkElement(config, 'backup-1')
const backupEl2 = createWatermarkElement(config, 'backup-2')
// 添加到页面
document.body.appendChild(watermarkEl)
document.body.appendChild(backupEl1)
document.body.appendChild(backupEl2)
// 存储备用元素
backupElements = [backupEl1, backupEl2]
// 保存原始样式
originalStyles = watermarkEl.style.cssText
// 创建水印实例
watermarkInstance = {
element: watermarkEl,
backupElements: backupElements,
config: config,
remove: removeWatermark,
update: function (newOptions) {
return createWatermark(Object.assign({}, config, newOptions))
},
}
// 启动多重防护机制
startProtection()
return watermarkInstance
}
/**
* 创建水印DOM元素
* @param {Object} config - 水印配置
* @param {string} suffix - 元素后缀标识
* @returns {HTMLElement} 水印元素
*/
function createWatermarkElement(config, suffix = 'main') {
// 创建canvas生成水印图案
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 设置字体
ctx.font = `${config.fontSize}px Arial, sans-serif`
ctx.fillStyle = config.fontColor
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
// 计算文本尺寸
const textMetrics = ctx.measureText(config.text)
const textWidth = textMetrics.width
const textHeight = config.fontSize
// 计算旋转后的尺寸
const radians = (config.rotate * Math.PI) / 180
const cos = Math.abs(Math.cos(radians))
const sin = Math.abs(Math.sin(radians))
const rotatedWidth = textWidth * cos + textHeight * sin
const rotatedHeight = textWidth * sin + textHeight * cos
// 设置canvas尺寸(包含间距),创建2x2错位排列
const singleWidth = rotatedWidth + config.gap[0]
const singleHeight = rotatedHeight + config.gap[1]
const canvasWidth = Math.ceil(singleWidth * 2)
const canvasHeight = Math.ceil(singleHeight * 2)
canvas.width = canvasWidth
canvas.height = canvasHeight
// 重新设置字体(canvas尺寸改变会重置样式)
ctx.font = `${config.fontSize}px Arial, sans-serif`
ctx.fillStyle = config.fontColor
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
// 绘制四个错位排列的水印,实现真正的错位效果
const positions = [
{ x: singleWidth * 0.5, y: singleHeight * 0.5 }, // 左上
{ x: singleWidth * 1.5, y: singleHeight * 0.3 }, // 右上(垂直错位)
{ x: singleWidth * 0.3, y: singleHeight * 1.5 }, // 左下(水平错位)
{ x: singleWidth * 1.3, y: singleHeight * 1.2 }, // 右下(双向错位)
]
positions.forEach((pos) => {
ctx.save()
ctx.translate(pos.x, pos.y)
ctx.rotate(radians)
ctx.fillText(config.text, 0, 0)
ctx.restore()
})
// 创建水印容器元素
const watermarkEl = document.createElement('div')
watermarkEl.setAttribute('data-watermark', 'true')
watermarkEl.setAttribute('data-watermark-id', suffix)
// 生成随机ID增加识别难度
const randomId = 'wm_' + Math.random().toString(36).substr(2, 9)
watermarkEl.id = randomId
const cssText = `
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
pointer-events: none !important;
background-image: url(${canvas.toDataURL()}) !important;
background-repeat: repeat !important;
background-position: ${config.offset[0]}px ${
config.offset[1]
}px !important;
z-index: ${suffix === 'main' ? 9999999 : 9999998} !important;
user-select: none !important;
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
opacity: ${suffix === 'main' ? 1 : 0.01} !important;
`
watermarkEl.style.cssText = cssText
return watermarkEl
}
/**
* 启动多重防护机制
*/
function startProtection() {
if (!protectionEnabled || !watermarkInstance) {
return
}
// 启动MutationObserver监听
startMutationObserver()
// 启动定时检查机制
startIntervalChecker()
// 启动样式监听
startStyleProtection()
}
/**
* 启动MutationObserver监听(增强版)
*/
function startMutationObserver() {
if (!window.MutationObserver || !watermarkInstance) {
return
}
observer = new MutationObserver(function (mutations) {
let needRestore = false
mutations.forEach(function (mutation) {
// 检查是否有节点被删除
if (mutation.type === 'childList') {
mutation.removedNodes.forEach(function (node) {
if (isWatermarkElement(node)) {
needRestore = true
}
})
}
// 检查水印元素的属性是否被修改
if (
mutation.type === 'attributes' &&
isWatermarkElement(mutation.target)
) {
// 检查关键属性是否被篡改
const target = mutation.target
if (
mutation.attributeName === 'style' ||
mutation.attributeName === 'class' ||
mutation.attributeName === 'id'
) {
needRestore = true
}
}
})
// 恢复水印
if (needRestore && watermarkInstance && protectionEnabled) {
const config = watermarkInstance.config
setTimeout(() => {
if (protectionEnabled) {
createWatermark(config)
}
}, 0)
}
})
// 监听整个文档,包括所有子树
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [
'style',
'class',
'id',
'data-watermark',
'data-watermark-id',
],
})
}
/**
* 启动定时检查机制
*/
function startIntervalChecker() {
if (intervalChecker) {
clearInterval(intervalChecker)
}
intervalChecker = setInterval(() => {
if (!protectionEnabled || !watermarkInstance) {
return
}
const mainElement = watermarkInstance.element
const backupElements = watermarkInstance.backupElements || []
// 检查主元素是否存在且正常
const mainExists = document.contains(mainElement)
const mainStyleValid = mainExists && isStyleValid(mainElement)
// 检查备用元素
const backupValid = backupElements.some(
(el) => document.contains(el) && isStyleValid(el)
)
// 如果主元素或备用元素被破坏,立即恢复
if (!mainExists || !mainStyleValid || !backupValid) {
const config = watermarkInstance.config
createWatermark(config)
}
}, 500) // 每500ms检查一次
}
/**
* 启动样式保护
*/
function startStyleProtection() {
if (!watermarkInstance) return
// 定期检查样式完整性
const styleChecker = setInterval(() => {
if (!protectionEnabled || !watermarkInstance) {
clearInterval(styleChecker)
return
}
const element = watermarkInstance.element
if (element && document.contains(element)) {
// 检查关键样式是否被篡改
const computedStyle = window.getComputedStyle(element)
if (
computedStyle.position !== 'fixed' ||
computedStyle.zIndex < '9999990' ||
computedStyle.pointerEvents !== 'none'
) {
// 样式被篡改,恢复水印
const config = watermarkInstance.config
createWatermark(config)
}
}
}, 1000) // 每1秒检查一次样式
}
/**
* 检查是否为水印元素
*/
function isWatermarkElement(node) {
if (!node || node.nodeType !== 1) return false
return (
node === watermarkInstance?.element ||
watermarkInstance?.backupElements?.includes(node) ||
node.getAttribute('data-watermark') === 'true'
)
}
/**
* 检查元素样式是否有效
*/
function isStyleValid(element) {
if (!element || !document.contains(element)) return false
const computedStyle = window.getComputedStyle(element)
return (
computedStyle.position === 'fixed' &&
parseInt(computedStyle.zIndex) >= 9999990 &&
computedStyle.pointerEvents === 'none' &&
computedStyle.width === '100%' &&
computedStyle.height === '100%'
)
}
/**
* 移除水印
*/
function removeWatermark() {
// 停用保护机制
protectionEnabled = false
if (watermarkInstance && watermarkInstance.element) {
const element = watermarkInstance.element
if (element.parentNode) {
element.parentNode.removeChild(element)
}
}
// 移除备用元素
if (watermarkInstance && watermarkInstance.backupElements) {
watermarkInstance.backupElements.forEach((el) => {
if (el.parentNode) {
el.parentNode.removeChild(el)
}
})
}
// 移除所有水印元素(防止重复)
const existingWatermarks = document.querySelectorAll(
'[data-watermark="true"]'
)
existingWatermarks.forEach((el) => {
if (el.parentNode) {
el.parentNode.removeChild(el)
}
})
watermarkInstance = null
backupElements = []
// 停止监听
if (observer) {
observer.disconnect()
observer = null
}
// 停止定时检查
if (intervalChecker) {
clearInterval(intervalChecker)
intervalChecker = null
}
}
// 将函数挂载到window对象
global.createWatermark = createWatermark
global.removeWatermark = removeWatermark
// 兼容模块化环境
if (typeof module !== 'undefined' && module.exports) {
module.exports = { createWatermark, removeWatermark }
}
// AMD支持
if (typeof define === 'function' && define.amd) {
define(function () {
return { createWatermark, removeWatermark }
})
}
})(typeof window !== 'undefined' ? window : this)
demo.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>水印防护功能测试页面</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.test-section {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
background: #f9f9f9;
}
.test-section h3 {
color: #4caf50;
margin-bottom: 15px;
}
button {
background: #4caf50;
color: white;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #45a049;
}
button.danger {
background: #f44336;
}
button.danger:hover {
background: #da190b;
}
.log {
background: #000;
color: #0f0;
padding: 10px;
margin: 10px 0;
border-radius: 5px;
font-family: monospace;
height: 200px;
overflow-y: auto;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
font-weight: bold;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
</style>
</head>
<body>
<div class="container">
<h1>🛡️ 水印防护功能测试</h1>
<div class="test-section">
<h3>1. 基础功能测试</h3>
<button onclick="createTestWatermark()">创建水印</button>
<button onclick="removeTestWatermark()" class="danger">移除水印</button>
<button onclick="checkWatermarkStatus()">检查水印状态</button>
</div>
<div class="test-section">
<h3>2. 防删除测试</h3>
<button onclick="testDeleteWatermark()" class="danger">
尝试删除水印元素
</button>
<button onclick="testHideWatermark()" class="danger">
尝试隐藏水印
</button>
<button onclick="testModifyStyle()" class="danger">
尝试修改水印样式
</button>
<button onclick="testRemoveAttributes()" class="danger">
尝试移除水印属性
</button>
</div>
<div class="test-section">
<h3>3. 高级攻击测试</h3>
<button onclick="testMassDelete()" class="danger">
批量删除所有水印
</button>
<button onclick="testStyleOverride()" class="danger">
样式覆盖攻击
</button>
<button onclick="testZIndexAttack()" class="danger">
z-index层级攻击
</button>
<button onclick="testOpacityAttack()" class="danger">透明度攻击</button>
</div>
<div class="status" id="status">等待测试...</div>
<div class="log" id="log">控制台日志将显示在这里...</div>
</div>
<script src="watermark-sdk.js"></script>
<script>
let watermarkInstance = null
let testCount = 0
function log(message, type = 'info') {
const logEl = document.getElementById('log')
const timestamp = new Date().toLocaleTimeString()
const logLine = `[${timestamp}] ${message}\n`
logEl.textContent += logLine
logEl.scrollTop = logEl.scrollHeight
console.log(message)
}
function updateStatus(message, isSuccess = true) {
const statusEl = document.getElementById('status')
statusEl.textContent = message
statusEl.className = 'status ' + (isSuccess ? 'success' : 'error')
}
function createTestWatermark() {
try {
watermarkInstance = createWatermark({
text: '测试水印 - 防删除保护',
fontSize: 18,
fontColor: 'rgba(255, 0, 0, 0.2)',
rotate: -30,
gap: [120, 80],
})
log('✅ 水印创建成功')
updateStatus('水印已创建,防护机制已启动', true)
} catch (error) {
log('❌ 水印创建失败: ' + error.message)
updateStatus('水印创建失败', false)
}
}
function removeTestWatermark() {
if (watermarkInstance) {
watermarkInstance.remove()
watermarkInstance = null
log('✅ 水印已正常移除')
updateStatus('水印已移除', true)
} else {
log('⚠️ 没有找到水印实例')
updateStatus('没有水印可移除', false)
}
}
function checkWatermarkStatus() {
const watermarks = document.querySelectorAll('[data-watermark="true"]')
log(`📊 当前页面水印元素数量: ${watermarks.length}`)
watermarks.forEach((wm, index) => {
const style = window.getComputedStyle(wm)
log(
` 水印${index + 1}: ID=${wm.id}, z-index=${
style.zIndex
}, opacity=${style.opacity}`
)
})
updateStatus(
`发现 ${watermarks.length} 个水印元素`,
watermarks.length > 0
)
}
function testDeleteWatermark() {
testCount++
log(`🔥 测试 ${testCount}: 尝试删除水印元素...`)
const watermarks = document.querySelectorAll('[data-watermark="true"]')
const initialCount = watermarks.length
if (initialCount === 0) {
log('❌ 没有找到水印元素')
updateStatus('测试失败:没有水印', false)
return
}
// 删除第一个水印元素
watermarks[0].remove()
log(`🗑️ 已删除水印元素 ${watermarks[0].id}`)
// 等待防护机制响应
setTimeout(() => {
const newWatermarks = document.querySelectorAll(
'[data-watermark="true"]'
)
const recovered = newWatermarks.length >= initialCount
log(
`📈 防护结果: 初始${initialCount}个 -> 当前${newWatermarks.length}个`
)
if (recovered) {
log('✅ 防护成功!水印已自动恢复')
updateStatus('防删除保护生效', true)
} else {
log('❌ 防护失败!水印未能恢复')
updateStatus('防删除保护失效', false)
}
}, 1000)
}
function testHideWatermark() {
testCount++
log(`🔥 测试 ${testCount}: 尝试隐藏水印...`)
const watermarks = document.querySelectorAll('[data-watermark="true"]')
if (watermarks.length === 0) {
log('❌ 没有找到水印元素')
return
}
const target = watermarks[0]
target.style.display = 'none'
log(`👻 已设置水印 ${target.id} 为 display: none`)
setTimeout(() => {
const style = window.getComputedStyle(target)
const isVisible = style.display !== 'none'
if (isVisible) {
log('✅ 防护成功!水印显示已恢复')
updateStatus('防隐藏保护生效', true)
} else {
log('❌ 防护失败!水印仍然隐藏')
updateStatus('防隐藏保护失效', false)
}
}, 1000)
}
function testModifyStyle() {
testCount++
log(`🔥 测试 ${testCount}: 尝试修改水印样式...`)
const watermarks = document.querySelectorAll('[data-watermark="true"]')
if (watermarks.length === 0) {
log('❌ 没有找到水印元素')
return
}
const target = watermarks[0]
const originalZIndex = target.style.zIndex
target.style.zIndex = '1'
target.style.opacity = '0'
log(`🎨 已修改水印样式: z-index=1, opacity=0`)
setTimeout(() => {
const style = window.getComputedStyle(target)
const zIndexRestored = parseInt(style.zIndex) > 1000000
const opacityRestored = parseFloat(style.opacity) > 0
if (zIndexRestored && opacityRestored) {
log('✅ 防护成功!样式已恢复')
updateStatus('防样式篡改保护生效', true)
} else {
log(
`❌ 防护失败!z-index=${style.zIndex}, opacity=${style.opacity}`
)
updateStatus('防样式篡改保护失效', false)
}
}, 1500)
}
function testRemoveAttributes() {
testCount++
log(`🔥 测试 ${testCount}: 尝试移除水印属性...`)
const watermarks = document.querySelectorAll('[data-watermark="true"]')
if (watermarks.length === 0) {
log('❌ 没有找到水印元素')
return
}
const target = watermarks[0]
target.removeAttribute('data-watermark')
target.removeAttribute('data-watermark-id')
log(`🏷️ 已移除水印属性`)
setTimeout(() => {
const hasAttribute = target.hasAttribute('data-watermark')
if (hasAttribute) {
log('✅ 防护成功!属性已恢复')
updateStatus('防属性移除保护生效', true)
} else {
log('❌ 防护失败!属性未恢复')
updateStatus('防属性移除保护失效', false)
}
}, 1000)
}
function testMassDelete() {
testCount++
log(`🔥 测试 ${testCount}: 批量删除攻击测试...`)
const watermarks = document.querySelectorAll('[data-watermark="true"]')
const initialCount = watermarks.length
if (initialCount === 0) {
log('❌ 没有找到水印元素')
return
}
// 删除所有水印
watermarks.forEach((wm, index) => {
wm.remove()
log(`🗑️ 删除水印 ${index + 1}/${initialCount}`)
})
setTimeout(() => {
const newWatermarks = document.querySelectorAll(
'[data-watermark="true"]'
)
const recovered = newWatermarks.length > 0
log(
`📈 批量删除结果: 初始${initialCount}个 -> 当前${newWatermarks.length}个`
)
if (recovered) {
log('✅ 防护成功!水印已重新创建')
updateStatus('批量删除防护生效', true)
} else {
log('❌ 防护失败!所有水印被删除')
updateStatus('批量删除防护失效', false)
}
}, 1500)
}
function testStyleOverride() {
testCount++
log(`🔥 测试 ${testCount}: 样式覆盖攻击测试...`)
// 创建覆盖样式
const style = document.createElement('style')
style.textContent = `
[data-watermark="true"] {
display: none !important;
opacity: 0 !important;
z-index: -1 !important;
}
`
document.head.appendChild(style)
log(`💉 已注入覆盖样式`)
setTimeout(() => {
const watermarks = document.querySelectorAll(
'[data-watermark="true"]'
)
let visibleCount = 0
watermarks.forEach((wm) => {
const computedStyle = window.getComputedStyle(wm)
if (
computedStyle.display !== 'none' &&
parseFloat(computedStyle.opacity) > 0 &&
parseInt(computedStyle.zIndex) > 0
) {
visibleCount++
}
})
if (visibleCount > 0) {
log('✅ 防护成功!样式覆盖被阻止')
updateStatus('防样式覆盖保护生效', true)
} else {
log('❌ 防护失败!样式被成功覆盖')
updateStatus('防样式覆盖保护失效', false)
}
// 清理测试样式
document.head.removeChild(style)
}, 2000)
}
function testZIndexAttack() {
testCount++
log(`🔥 测试 ${testCount}: z-index层级攻击测试...`)
// 创建高层级遮挡元素
const blocker = document.createElement('div')
blocker.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 99999999;
pointer-events: none;
`
document.body.appendChild(blocker)
log(`🚧 已创建z-index=99999999的遮挡层`)
setTimeout(() => {
const watermarks = document.querySelectorAll(
'[data-watermark="true"]'
)
let higherZIndexCount = 0
watermarks.forEach((wm) => {
const zIndex = parseInt(window.getComputedStyle(wm).zIndex)
if (zIndex > 99999999) {
higherZIndexCount++
}
})
if (higherZIndexCount > 0) {
log('✅ 防护成功!水印z-index已提升')
updateStatus('防z-index攻击保护生效', true)
} else {
log('❌ 防护失败!水印被遮挡')
updateStatus('防z-index攻击保护失效', false)
}
// 清理测试元素
document.body.removeChild(blocker)
}, 1500)
}
function testOpacityAttack() {
testCount++
log(`🔥 测试 ${testCount}: 透明度攻击测试...`)
const watermarks = document.querySelectorAll('[data-watermark="true"]')
if (watermarks.length === 0) {
log('❌ 没有找到水印元素')
return
}
// 设置所有水印为完全透明
watermarks.forEach((wm, index) => {
wm.style.opacity = '0'
log(`👻 设置水印${index + 1}透明度为0`)
})
setTimeout(() => {
let visibleCount = 0
watermarks.forEach((wm) => {
const opacity = parseFloat(window.getComputedStyle(wm).opacity)
if (opacity > 0) {
visibleCount++
}
})
if (visibleCount > 0) {
log('✅ 防护成功!透明度已恢复')
updateStatus('防透明度攻击保护生效', true)
} else {
log('❌ 防护失败!水印仍然透明')
updateStatus('防透明度攻击保护失效', false)
}
}, 1500)
}
// 页面加载完成后自动创建测试水印
window.addEventListener('load', () => {
log('🚀 页面加载完成,开始测试...')
setTimeout(createTestWatermark, 500)
})
</script>
</body>
</html>