字形混淆:浏览器里的攻与防

免责声明:本文以及文中涉及的代码仅用于个人学习交流,禁止转载。同时请勿将本文介绍的相关技术用于非法用途,对于非法用途或者滥用相关技术而产生的风险和相关的法律后果,与本文作者无关。请在阅读本文内容时,确保仅用于个人学习交流,并遵守当地法律法规。

故事的开始

不知道大家有没有用过一些剪藏助手的浏览器插件,就可以把在网页上看到内容,通过插件将内容收藏起来。我自己平时就会把这些自己觉得有收藏价值的内容,通过印象笔记、金山文档等剪藏助手收藏起来,供以后翻看。

但是前段时间,在剪藏一篇文章时,发现了一个奇怪的事情。我打开了一个网站,这个网站是讲历史的,在拿出我的剪藏助手想要收藏时,遇到了下面的问题。这里我随便找一篇这个网站的文章作为例子截图演示一下:

以这篇文章为例演示一下

但是剪藏完之后一看,发现不对的地方了,怎么句子都读不通顺?难道剪藏助手出bug了?

语句都读不通顺的内容

要知道,所谓的剪藏助手,它的原理就是读取网页的HTML,把里面的文字和样式获取到然后塞到自己新创建的文档里,正常来讲不应该有问题的。打开这个网站浏览器开发者工具看一下:

DOM结构中的内容,和最终渲染的内容不一样

可以看到DOM中的内容,和最终展示的内容确实不一样,而剪藏助手截取的内容,也确实是DOM中的内容无疑,并不是剪藏助手的bug。

这就奇怪了,为啥内容会不一样呢?明明就是很简单的纯文字渲染,也没有什么魔法在里面。
这个时候我注意到右下角有一个CSS属性:font-family,它的值是很奇怪的一个内容:

一串好像是编码后的字符串

初现端倪

这个font-family看起来有点奇怪,我们来尝试改一下看看。

修改font-family为其他字体

可以看到,在改了字体之后,渲染出来的内容,和DOM里的文字就对得上了,同样的读不通顺的句子。并且在来回切换字体的时候,页面渲染出来的内容也随着字体变化而变化:

渲染的UI随着font-family变化

看来这个font-family就是问题所在。

揭开面纱

既然找到了问题所在,那么我们来看下这个有点特别的font-family是什么,既然不是一个标准字体,那就应该是用户自定义的字体了,自定义在DOM中全局搜一下这个字体:

自定义字体

可以看到这个字体是通过base64的方式引入的,并且内容也恰好是data:font/ttf这样一个字体格式。

TTF(TrueType Font),一种二进制格式的文件。TTF 文件包含字体的矢量信息、度量信息和其他相关数据,用于在计算机上显示和打印文本。

我们知道,base64是可以解码的,那我们把这段base64复制下来,还原为二进制ttf文件,可以参考下面的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function base64ToArrayBuffer(base64) {
const binaryString = atob(base64.split(',')[1]);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}

function saveArrayBufferToFile(buffer, filename) {
const blob = new Blob([buffer], { type: 'font/ttf' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}

// 示例用法
const base64Data = 'data:font/ttf;base64,AAEAAAALAIAAAwAwT1MvMg8S...'; // 你的 base64 编码数据
const arrayBuffer = base64ToArrayBuffer(base64Data);
saveArrayBufferToFile(arrayBuffer, 'font.ttf');

或者通过网上的一下小工具来实现这一步:将base64格式的字体信息解码成可用的字体文件

ttf字体文件

用一些字体编辑器打开这个字体文件发现,这个字体文件里有一些自定义的字形(Glyphs)

字体文件包含自定义字形

而如果我们去了解一下字体相关的知识的话,就会知道:

字符是Unicode码位标识的逻辑单元,而字形则是字体的图形单元。

Characters are logical text units identified by Unicode code points, whereas glyphs are graphical font units.

字符和字形

我们都知道,Unicode又叫万国码,是Unicode联盟整理和编码的字符集,可以用来展示世界上大部分的文字系统。

Unicode编码从0开始,为每一个字符分配一个唯一的编号,这个编号就叫做码位(code point,也叫码点),以“计算机”的“机”字举例,“机”的码位为26426(十进制),即Unicode字符集中第26426个字符,就是“机”。

但是在表示一个Unicode的字符时,通常会用U+然后紧接着一组十六进制的数字来表示这一个字符。也就是说,“机”在Unicode中的表示方式是U+673A,码位是673A

更多关于字符编码的知识可以看这篇文章:
字符编码简史:从二进制到UTF-8

但是这个Unicode编码只是一个逻辑单元,在逻辑上给每个字符分配了一个编号。真正要展示的时候,还是需要给每个编号对应的字符,一个真正的图形,去展示去渲染,这个图形就是所谓的字形。

而在字体文件中,比如ttf,字形通常可以用SVG来标识,或者是使用一种叫做 “字形描述语言”(Glyph Description Language)的语言来描述。这种语言可以像SVG一样描述复杂的字形,包括直线、曲线和其他形状。

在字体文件中,每个字形都有一个唯一的标识符,这个标识符通常就是这个字符的Unicode码位。字形和Unicode码位之间的映射关系是在字体文件中定义的。

当浏览器读取到一个字体文件时,往往会进行以下步骤来渲染对应的字体:

  1. 解码字体数据:浏览器会通过CSS的font-face拿到字体文件或者字体的base64编码字符串,解析得到字体文件的原始数据
  2. 加载字体:浏览器将解码后的字体数据加载到内存中,然后解析字体文件,读取其中的字形和对应的Unicode码位
  3. 渲染字体:当需要渲染一个字符时,浏览器会使用这个字符的Unicode码位,去字体文件中找到对应的字形,然后使用对应的字形来渲染这个字符

谜底

到这里,谜底差不多就已经揭晓了。

既然每个字符在渲染的时候,都有一个字形去控制渲染的形状,那么同一个字符可以根据不同的风格以不同的方式渲染或绘制,比如字母A,在不同的字体下可能有不同的形状:

不同字体下的“A”

同时从另一个角度看,有时字形可能看起来相同,但代表不同的字符。例如,拉丁字母A、西里尔字母А 和希腊大写字母Alpha Α看起来都一样,但代表不同文字的不同字符。

那更进一步呢?我们是不是在一个字体里,可以把字符“A”的字形,映射到字形“B”,把字符“B”的字形,映射到字形“C”?这样的话一段字符串“AB”,最终浏览器渲染出来的,会是“BC”。

答案揭晓,上面剪藏内容对不上,就是使用了这种技术,这种常被称为“字形替换”或“字形混淆”,或者换一个爬虫领域常用的名字,“字体反爬”。

它的原理就是使用一个特殊的字体,这个字体将每个字符映射到一个不同的字形。例如,字体可能将字符“A”映射到字形“B”,将字符“B”映射到字形“C”,等等。

这样,当用户查看网页时,可以将看似乱码的内容,映射到正确的文本,用户会看到正确的文本。但是当他们尝试复制和粘贴文本时,或者说通过剪藏助手这种爬虫工具爬取页面内容时,得到的就是错误的文本。

我们来简单验证一下这个结论:

随便找几个字

随便找几个对不上的字看一下,用红框圈出来,去看下字体文件里这几个字的字形:

字体文件里对应的字形

可以看到果然这些字都在自定义字形里,每个字的左下角是字符,中间是字形,这些字形恰好对应上了上面浏览器里的渲染结果。

点开一个字查看具体信息也可以看到,比如“管”这个字:

“管”这个字

码位是U+7BA1,去第三方网站上搜一下这个码位对应的字符也确实是“管”:

U+7BA1

而这个字符的字形,确实是“这”的形状:

“管”对应“这”的形状

“这”的形状矢量图

可以看到是类似于SVG的这种矢量图,我们还可以自己随便拖拽这些点,改变字形:

被编辑的“这”的矢量图

而如果有兴趣的话,还可以自己画或者设计一款专属字体,比如我下面自己用鼠标随便画了一个形状,字母“a”的字形,有点丑:

自定义“a”

导出这个ttf文件并在CSS中引用,就可以看到我们自己的字体了:

使用自定义“a”

攻与防

到这里已经把上面遇到的“奇怪”事情搞清楚了,这个网站通过这种技术,来实现反爬虫,防止爬虫抓取内容,也可以防止剪藏助手这类工具剪藏一些内容,因为即便爬虫抓取或者剪藏了HTML里的内容,拿到的也是混淆后的错误内容,想要看到正确的内容,只能通过实时网络获取编码后的字体,来渲染为正确的内容。

可以说内容的“正确”,只会体现在浏览器的渲染层面,数据层面本身就是错误的。并且,这个字形的混淆以及对应的字体,每次请求都不一样。即每次加载页面请求服务端时,服务端都会先预先对内容进行一次混淆,每次使用的混淆字形都是不同的字符和字形。

通过上面这些操作,防止了第三方获取到本站的内容,本站内容只能在本站查看,提高了网站的用户留存率之类的。

即便爬虫在每次抓取内容时,获取对应的字体文件,也很难解析到字符和字形的对应关系,大大提高了爬虫爬取的成本。

但是上面说的这些,都是“防”,是一种防御手段,还记得标题吗,“攻与防”,“攻”体现在哪里?

其实在知道了字形混淆的原理,一些钓鱼网站可以用这种方式来进行攻击,比如:

  • 钓鱼网站通过字形混淆来绕过一些安全检查器,检查器获取到的是无意义的乱码,但是用户看到的是可读的文字,通过这种方式来进行钓鱼攻击
  • 将恶意Linux命令伪装成安全命令(使rm -rf /h*看起来像echo hello),粗心的用户可能直接复制粘贴到终端并执行,造成严重后果
  • 通过将垃圾邮件伪装成有用邮件来绕过垃圾邮件过滤
  • 通过内嵌某种字体到pdf文档中(pdf可以内嵌字体,发给其他人查看时不需要其他人在他们电脑上安装对应字体),来绕过某些对文档、论文等内容的查重处理,比如将字形进行混淆,查重工具读到的是乱码,那么查重结果自然不会有什么问题,同时通过pdf内嵌的字体,保证了人在看文档的时候的可读性(注:抄袭剽窃可耻,学术造假侵权违法,不要这样做

上面这些都是利用字形混淆的方式来进行攻击的场景,我们这里只是讨论和学习这种技术可能存在的攻击手段,不是教导大家去这样搞,法网恢恢疏而不漏,大家不要去做这种攻击的事情

同时这也提醒我们在复制网上一些内容时,一定要检查内容的正确性。

自己尝试实现一下

在知道了上面的原理后,我们自己也可以实现字形混淆,但是总不能手动编辑每个字的字形矢量图吧,而且像是上面那种每次打开都会重新对不同字符的字形进行混淆的,肯定是需要把字形混淆的操作脚本化的,那假设我们自己也有一个网站需要对内容进行保护,我们要怎么实现上面的字形混淆逻辑呢?

我们的目标是:对于一段提供好的文本,针对其中部分字符或者全部字符,进行字形混淆,并生成混淆后的文本以及对应的字体文件。

预期处理流程

整体思路是:

  1. 准备一个基础字体文件
    这个基础字体文件用于获取字体中字符信息和字形信息。
  2. 确定好原始文本中需要进行混淆的字符列表
    比如对于原始文本“ABCD”,需要对其中的“ABC”这三个字符进行混淆。
  3. 确定好混淆的目标字符列表
    比如上面“ABCD”中,对“ABC”三个字符进行混淆,分别混淆成“XYZ”。最终生成“XYZD”,但是展示出来的样子还是“ABCD”的样子。
  4. 从基础字体文件中,获取字形信息
    获取原始字符(比如“A”)和混淆的字符(比如混淆成“X”)各自的字形信息
  5. 替换字形形状的矢量图
    将混淆字符(“X”)的字形信息中,描述字形形状的矢量图路径,替换为原始字符(“A”)的字形信息中的描述字形形状的矢量图路径。
    把所有需要替换字形形状的字符(“ABC”)都替换完。
  6. 反向替换字形形状的矢量图
    但是把“XYZ”的字形形状改成“ABC”之后,真的需要展示“XYZ”的形状了怎么办?这个时候就需要进行反向混淆,把“ABC”这三个字符,随机指向“XYZ”的字形的形状。
    比如字符“A”的字形形状改为“Y”,字符“B”的字形形状改为“Z”,字符“C”的字形形状改为“X”。操作方法同上。
    这里随机指向而不是直接“A”指向“X”,“B”指向“Y”,“C”指向“Z”,也是为了加大破解难度,如果字符比较多的话,比如100个字符的字形随机相互指向,那么在没有字体文件的情况破解难度会很大。
  7. 修改原始文本中的字符为混淆后的字符
    把原始文本中的字符,根据上面的字形替换映射关系,改成对应的混淆字符,生成最终混淆后的文本。即,把“ABCD”编辑修改为“XYZD”,在DOM中写“XYZD”即可。

最终效果就是,在DOM中写“XYZD”,展示出来的时候,展示为“ABCD”,如果需要展示“XYZ”本身,那么在DOM中写“CBA”。

下面给出一份代码Demo,根据提供的文本,随机从中间选出一些字符作为需要进行混淆的字符。然后从100个最常用的中文字符中,随机选出相同数量的字符作为混淆的目标字符。并最终返回混淆后的文本、字体文件,以及对应的映射关系。

支持指定混淆的等级,分别进行低、中、高不同程度以及全部文本字符的混淆:

fontObfuscateRandom.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
const opentype = require('opentype.js');
const fs = require('fs');
const path = require('path');
const { frequentlyUsedCnChar } = require('./const');

const BASE_FONT_PATH = path.resolve(__dirname, './微软雅黑.ttf');
const OUTPUT_PATH = path.resolve(__dirname, '../output');

/**
* Randomly pop an element from the given array, and return it. Will modify the original array.
* @param {*} arr The array
* @returns {*} The popped element
*/
function arrRandomPop(arr) {
const index = Math.floor(Math.random() * arr.length);
return arr.splice(index, 1)[0];
}

/**
* Get specified count of random characters from the given list, excluding the characters in the except list
* @param { string[] } charList The list of characters
* @param { number } count The count of characters to get
* @param { string[] } exceptList The list of characters to exclude
* @returns { string }
*/
function getRandomCharsFromList(charList, count, exceptList = []) {
const charSet = new Set(charList);
const result = [];
while (result.length < count) {
const index = Math.floor(Math.random() * charSet.size);
const randomChar = Array.from(charSet)[index];
if (exceptList.includes(randomChar)) {
continue;
}
result.push(randomChar);
charSet.delete(randomChar);
}
return result;
}

/**
* @typedef { Object } IObfuscatePlusParams
* @property { string } originalText The original text that need to obfuscate
* @property { 'low' | 'middle' | 'high' | 'all' } obfuscateLevel The level of obfuscation
* @property { string } fontName The name of the font family
* @property { string } fontFilename The name of the output font file
*/

/**
* Obfuscate a font by replacing the paths of glyphs with the paths of other glyphs
*
* @param { IObfuscatePlusParams } params
* @returns { Promise<{ obfuscatedText: string, fontFilePath: string, obfuscatedMap: { [key: string]: string } }> }
*/
function obfuscateRandom(params) {
const { originalText, obfuscateLevel, fontName, fontFilename } = params;
const allCharsSet = new Set(originalText);
const levelMap = { low: 0.3, middle: 0.5, high: 0.7, all: 1 };
const countOfLevel = Math.round(allCharsSet.size * levelMap[obfuscateLevel]);
const count = Math.min(countOfLevel, frequentlyUsedCnChar.length);

const obfuscateMap = {};
const oppositeMap = {};
const displayChars = getRandomCharsFromList(originalText, count);
const obfuscateChars = getRandomCharsFromList(
frequentlyUsedCnChar,
count,
displayChars
);

for (let i = 0; i < count; i++) {
const displayChar = displayChars[i];
const obfuscateChar = obfuscateChars[i];
obfuscateMap[displayChar] = obfuscateChar;
}
obfuscateChars.forEach(obfuscateChar => {
const randomDisplayChar = arrRandomPop(displayChars);
oppositeMap[obfuscateChar] = randomDisplayChar;
});

const finalMap = { ...obfuscateMap, ...oppositeMap };
const finalObfuscateList = Object.keys(finalMap).map(key => ({
displayChar: key,
obfuscatedChar: finalMap[key],
}));

const obfuscatedText = originalText
.split('')
.map(char => finalMap[char] || char)
.join('');

return new Promise((resolve, reject) => {
opentype.load(BASE_FONT_PATH, (err, font) => {
if (err) {
console.error('Could not load font: ' + err);
reject(err);
return;
}

const obfuscatedGlyphs = finalObfuscateList.map(item => {
const { displayChar, obfuscatedChar } = item;
// Get the glyph for the display character and the obfuscated character
const displayGlyph = font.charToGlyph(displayChar);
const obfuscatedGlyph = font.charToGlyph(obfuscatedChar);
// Clone the obfuscated glyph and replace its path with the display glyph's path
const cloneObfuscatedGlyph = Object.assign(
Object.create(Object.getPrototypeOf(obfuscatedGlyph)),
obfuscatedGlyph
);
cloneObfuscatedGlyph.path = displayGlyph.path;
cloneObfuscatedGlyph.name = displayChar;
return cloneObfuscatedGlyph;
});

// This .notdef glyph is required, it's used for characters that doesn't have a glyph in the font
const notDefGlyph = new opentype.Glyph({
name: '.notdef',
advanceWidth: 650,
path: new opentype.Path(),
});

// Create a new font object with the obfuscated glyphs
const newFont = new opentype.Font({
familyName: fontName,
styleName: 'Regular',
unitsPerEm: font.unitsPerEm,
ascender: font.ascender,
descender: font.descender,
glyphs: [notDefGlyph, ...obfuscatedGlyphs],
});

// Save the new font to a file
const outputFilePath = path.resolve(OUTPUT_PATH, `${fontFilename}.ttf`);
const buffer = newFont.toArrayBuffer();
fs.writeFileSync(outputFilePath, Buffer.from(buffer));
resolve({
obfuscatedText,
fontFilePath: outputFilePath,
obfuscatedMap: finalMap,
});
});
});
}

async function start() {
const result = await obfuscateRandom({
originalText: '找到夺嫡密码的,自始至终都只有一个人而已。',
obfuscateLevel: 'middle',
fontName: 'MyObfuscatedFont',
fontFilename: 'my-obfuscated-font',
});
console.log('obfuscatedText: \n', result.obfuscatedText);
console.log('\n');
console.log('fontFilePath: \n', result.fontFilePath);
console.log('\n');
console.log('obfuscatedMap: \n', result.obfuscatedMap);
}

start();

运行之后效果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ node ./src/fontObfuscateRandom.js
obfuscatedText:
找到部嫡进码的说量多至终机们点一个来分是。


fontFilePath:
/Users/your_user_name/font-obfucate-demo/output/my-obfuscated-font.ttf


obfuscatedMap:
{
'只': '们',
'密': '进',
'而': '分',
'已': '是',
'夺': '部',
'始': '多',
'都': '机',
',': '说',
'有': '点',
'人': '来',
'自': '量',
'们': '已',
'进': '只',
'分': '自',
'是': '都',
'部': '而',
'多': ',',
'机': '有',
'说': '夺',
'点': '密',
'来': '人',
'量': '始'
}

在使用的时候如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<style>
@font-face {
font-family: MyObfuscatedFont;
src: url('my-obfuscated-font.ttf') format('truetype');
}
.myFont {
font-family: 'MyObfuscatedFont';
}
</style>
</head>
<body>
<p class="myFont">找到部嫡进码的说量多至终机们点一个来分是。</p>
</body>
</html>

最终效果:

最终效果

选中这段文字进行复制,复制到剪贴板的内容也是DOM中的“找到部嫡进码的说量多至终机们点一个来分是。”。

如果打开生成的字体文件看一下的话,会发现和上面讨论的那个网站的字体文件中的效果也是类似的:

最终字体文件

有我们生成的自定义的字形,并且和上面DOM和渲染中的结果也是一一对应的。

而如果进行了最大程度的混淆,即对原始文本中所有字符混淆,最终混淆的内容将完全不可读:

完全混淆

而且由于是随机选择文本和目标字符进行混淆的,每次执行混淆脚本,都会生成完全不同的字体和混淆文本,这个也和上面内容里提到的效果对得上。

到此为止,我们也算是揭开了字形混淆的“神秘面纱”,也基本上通过代码实现了类似的效果,还可以进一步做的事情就是把字体文件进行base64编码并通过base64字符串来在CSS里引用了。

那除了上面的这些场景,字形混淆还有哪些用法呢?

一些其他的用法

字形混淆除了用在上面的场景外,还有一种无关反爬虫或者恶意攻击的用法,那就是字体图标,比如比较出名的iconfont,大家可能还用过。

这是阿里提供的一个矢量图标库,可以下载各种矢量图,同时也支持通过字体的方式来渲染图标。原理就是上面提到的,把一些字符的字形,改为一些icon图标的形状,在用这个图标的地方就可以通过下面这种方式来使用:

  1. 声明字体
1
2
3
4
@font-face {
font-family: 'iconfont';
src: url('iconfont.tff') format('tff');
}
  1. 引用字体
1
2
3
4
5
6
.iconfont {
font-family: "iconfont";
font-size: 16px;
font-style: normal;
color: red;
}
  1. 将字符渲染成icon
1
<span class="iconfont">&#xe650;</span>

这样就可以渲染出这个字符对应的图标,比如我这里:

iconfont demo

这里有一个点要说一下,因为是使用了替换某些字符的字形来实现的icon,为了不影响这些字符的正常展示,肯定是不能把常用中文或者英文等字符的字形来进行替换的,所以目前我看到的是他们使用了Unicode第一平面内的私人使用区的一些字符:

私人使用区

私人使用区

可以看到上面span里的字符&#xe650;是用的Unicode的HTML实体字符的形式来写的,码位是e650,可以看到e650正好是落在了第一平面内的私人使用区范围内:

U+E650

而顾名思义,私人使用区就是没有公共定义的Unicode区域,这些区域没有在Unicode标准中指定对应的具体字符是哪个,可以由个人或组织根据需要自行使用。把这些私人使用区的字符的字形,在字体文件中指定为需要使用到的icon,就可以做到不影响常用字符的渲染和展示了。

而且私人使用区内的字符,因为没有真正的字符来对应,在自定义字体文件没有加载好的时候,也不用担心会使用默认字体渲染出其他字符,而是会展示出一个框,就像上面DOM结构里看到的那样。

那这种字体图标的好处在哪里呢?为什么不直接使用图片?其实目前看下来有以下几个优势:

  1. 相比于图片,字体是矢量的,可以随意缩放而不影响图片的清晰度,在不同分辨率的屏幕上表现都很好
  2. 即使是图片中的矢量图SVG,体积大小也比字体文件中专门优化过的、效率更高的字形描述语言描述的字形,体积要更大,做了一个对比,同样是25个上面的那种图标,字体文件要比SVG的体积小上很多

字体文件和SVG体积对比

  1. 减少网络请求次数,如果每个SVG单独下载,那么会发起多个网络请求,但是如何合并到一个字体文件里,只需要一次网络请求即可
  2. 图标作为字体来渲染,可以像操作字体一样来操作图标,比如设置颜色、字重、斜体、字号等,常规图片展示则没办法通过这些来控制

使用字体属性控制icon

结语

到这里这篇文章就差不多结束了,简单总结一下字形混淆在各个场景的应用:

1. 内容保护

可以用在内容保护上,防止爬虫爬取或者文档剪藏工具剪藏,保护网站内容,不过从其他角度多想一下,感觉这种技术也不是没有缺点,比如下面的:

  • 可访问性:这种技术可能会影响到屏幕阅读器等辅助技术的使用,因为这些工具通常会读取DOM中原始的字符,而不是渲染后的字形
  • 搜索引擎优化(SEO):搜索引擎通常会索引网页的原始文本,而不是渲染后的文本。使用字形替换技术可能会影响到网站在搜索结果中的排名
  • 用户体验:如果用户需要引用网站的内容,这种技术就可能会带来不好的用户体验。而且从防爬防复制角度来说,虽然这种技术可以防止用户直接复制文本,但是它不能防止用户通过其他方式获取文本,例如通过截图和OCR技术,甚至是物理拍照的方式,只能在某种程度上保护网站内容。

另外除了字形混淆外,还有其他方式来保护内容,比如用canvas来渲染所有的文本内容,canvas元素本身只是一个绘图区域,里面的内容是通过JS绘制的,绘制的文本实际上是作为图像的一部分存在的,不会直接出现在HTML文档中。但是这种方案仍然存在上面的几个潜在问题。

综合来讲还是要根据自己的业务场景来区分是否要混淆,如果需要提高用户留存率,甚至是保护一些付费内容的话,还是很有字形混淆的必要性的。

2. 恶意攻击

一些钓鱼网站或者垃圾邮件,可能通过这种技术来绕过检测和拦截进行攻击。同时我们要注意不受信的网站上复制的内容要注意也是不可信的,可能复制到非预期的危险内容,需要注意鉴别。

3. icon图标

通过对一些字符进行字形替换,可以用字体的方式来控制和渲染icon图标,做到比直接使用图片更好的展示效果和更低的开发成本。

这篇文章算是上一篇文章《字符编码简史:从二进制到UTF-8》的一个延伸,分别从字符和字形的角度,探寻日常编程中那些不易察觉的微妙场景,感觉还是很有趣的。

参考链接

免责声明:本文以及文中涉及的代码仅用于个人学习交流,禁止转载。同时请勿将本文介绍的相关技术用于非法用途,对于非法用途或者滥用相关技术而产生的风险和相关的法律后果,与本文作者无关。请在阅读本文内容时,确保仅用于个人学习交流,并遵守当地法律法规。