在前端版权、数据防泄露这种场景,我目前一般都是采用显性水印的方案。
包括目前企业微信这种级别的App也是在用显性水印。
但是显性水印的问题在于:要么遮挡页面内容、破坏视觉体验,要么极易被PS去除。
零宽字符水印凭借肉眼完全不可见、复制粘贴不丢失的核心优势,完全解决了显性水印的痛点。
实现原理
零宽字符属于Unicode标准内的特殊控制字符,这类字符无视觉渲染、不占用页面宽度、不影响文本排版。
日常浏览、复制时完全无法察觉,但会被浏览器、编辑器、各类平台识别并保留。
-
U+200B(零宽空格):用于指代二进制 0
-
U+200C(零宽不连字):用于指代二进制 1
-
U+FEFF(零宽断行符):作为水印前缀标记,提升解析准确率,避免误识别
基于这种特性,加密就是将用户ID进行二进制转换,插入到文本中。
解密就是在文本中识别出零宽字符,再还原为普通字符。
完整代码
简单封装了一个面向对象封装的零宽水印工具类,复制进生产环境可直接使用,支持文本加密、水印解密两大核心功能。
/**
* 零宽字符隐形水印工具类
* 核心功能:文本水印加密、隐形水印解密
* 适用场景:前端版权保护、内容溯源、防搬运追责
*/
class ZeroWidthWatermark {
static #ZERO_CHAR = '\u200B'; // 零宽空格 = 二进制0
static #ONE_CHAR = '\u200C'; // 零宽不连字 = 二进制1
static #WATERMARK_PREFIX = '\uFEFF'; // 水印前缀校验符
/**
* 加密:给文本添加隐形水印
* @param {string} text - 原始文本内容
* @param {string} watermark - 水印信息(用户ID、溯源标识等)
* @returns {string} 带隐形水印的文本
*/
static encrypt(text, watermark) {
if (!text || typeof text !== 'string' || !watermark) {
throw new Error('加密失败:文本与水印内容不可为空');
}
try {
const binaryWatermark = this.#textToBinary(watermark);
const zeroWidthStr = this.#binaryToZeroWidth(binaryWatermark);
const fullWatermark = this.#WATERMARK_PREFIX + zeroWidthStr;
return text[0] + fullWatermark + text.slice(1);
} catch (error) {
console.error('零宽水印加密异常:', error);
return text;
}
}
/**
* 解密:提取文本中的隐形水印
* @param {string} encryptedText - 带水印的文本
* @returns {string} 解析后的水印信息/状态提示
*/
static decrypt(encryptedText) {
if (!encryptedText || typeof encryptedText !== 'string') {
return '无水印';
}
try {
const zeroWidthChars = encryptedText.match(/[\u200B\u200C\uFEFF]/g) || [];
if (zeroWidthChars.length === 0) return '无水印';
const prefixIndex = zeroWidthChars.indexOf(this.#WATERMARK_PREFIX);
if (prefixIndex === -1) return '无水印';
const validChars = zeroWidthChars.slice(prefixIndex + 1);
const binaryStr = this.#zeroWidthToBinary(validChars);
if (!binaryStr || binaryStr.length % 8 !== 0) return '水印格式错误';
return this.#binaryToText(binaryStr);
} catch (error) {
console.error('零宽水印解密异常:', error);
return '解密失败';
}
}
/** 文本转8位二进制字符串 */
static #textToBinary(text) {
return Array.from(text)
.map(char => char.charCodeAt(0).toString(2).padStart(8, '0'))
.join('');
}
/** 二进制转零宽字符 */
static #binaryToZeroWidth(binary) {
return binary.split('').map(bit => bit === '0' ? this.#ZERO_CHAR : this.#ONE_CHAR).join('');
}
/** 零宽字符转二进制 */
static #zeroWidthToBinary(chars) {
return chars.map(char => char === this.#ZERO_CHAR ? '0' : '1').join('');
}
/** 二进制转普通文本 */
static #binaryToText(binary) {
return Array.from({ length: binary.length / 8 }, (_, i) => {
const byte = binary.slice(i * 8, (i + 1) * 8);
return String.fromCharCode(parseInt(byte, 2));
}).join('');
}
}
使用示例
// 加密:添加隐形溯源水印
const originalText = "李剑一原创技术文章,禁止未经授权搬运转载";
const watermarkInfo = "userID:10086|publishTime:20260326|from:李剑一";
const watermarkedText = ZeroWidthWatermark.encrypt(originalText, watermarkInfo);
// 解密:提取溯源信息
const result = ZeroWidthWatermark.decrypt(watermarkedText);
console.log('解析出水印信息:', result);
总结
零宽水印能够无侵入式版权保护,而且在前端层面上实现还是比较简单的。
但是需要明确:零宽水印无法直接阻止爬虫爬取内容,因为爬虫会直接抓取页面文本,连带零宽字符一同获取。
如果需要防爬,可将零宽水印与字体加密、接口签名、行为验证、IP限流等方案结合使用,兼顾防护与溯源。