谈一谈使用字体库加密数据仿58同城

42次阅读

共计 9894 个字符,预计需要花费 25 分钟才能阅读完成。

对于前端同学来说其实做的更多的事情就是把数据整合好,按照 UI 同学的设计通过后端同学给的数据展示在网页中,这也就导致了很多人认为前端很简单,没有做什么工作也没有什么后端复杂的业务逻辑。

其实不然前端要做的工作有很多,就比如今天要说的,如何做到数据的反爬,笔者最近也接到了相同的任务,公司的数据频繁被爬虫爬走,出现这个情况之后之后就开始调研如何才能实现前端业务数据的反爬工作。刚刚开始接到这个需求的时候,不知道该如何处理这件事,只能硬着头皮接下了这个任务。

接到任务之后就开始了各种Google,笔者觉得如果想要做到反爬就要先知道什么是爬虫以及爬虫是如何作业。这就好比我们要足够的了解对手才能知道如何去防御。

爬虫是通过发送 http 请求获取获取到响应后的内容,通过按照一定的规则,自动爬去网页信息,之后保存数据的过程。

笔者在刚刚开始的时候也写了一个小小的爬虫实践了一下,大概就是发送一个 get 请求,然后通过类似于 DOM 操作的东西,找到网页中所需要的数据,然后进行存储。

分析 58 案例

在调研过程中发现很多博客都是针对爬取 58 同城的网页数据进行了分析,于是就点进去看了一下。58是使用的字体对数据进行加密的。所见的和所实际展示的内容是不一致的。看上去很是高端的样子。

这是什么情况?于是查看了一下当前的元素的 CSS 样式,可以注意到这样的元素使用了奇怪的 font-family:fangchan-secret(房产 - 加密) 字体样式,如果我们关闭这个 strongbox 样式,停用这个字体,页面上就会如实的显示乱码了。

其实可以看的出来 58 同城是使用了 font-family 字体进行一次加密处理,笔者在 Network 中找了很久也没有找到这个有关这段的字体文件。。。啊嘞?那么字体是哪来的。。。于是笔者就去查看了一下 font-family@font-face这个 CSS 的相关文献。

原来 @font-face 不单单可以接收一个文件的地址,还可以使用 base64 作为 src 中的参数。于是在 58 同城的页面中查看源码,果真和我想的一样,58同城没有通过拉取字体库资源处理,而是在页面最开始创建时通过 JavaScript 脚本动态添加入到页面中。

大概知道 58 的骚操作之后就开始研究下一步的东西,如何实现文字所见和实际不同的。其实对于每个汉字和字符来都对应了一个 Unicode 编码,从第一张图片中不难看出查看源码时 &#x9476 这些就是 Unicode 编码,浏览器通过 Unicode 在字体库中找到对应的文字。这个就和平时使用的字体图标库道理差不多吧(个人觉得。。。哈哈哈)。

打开百度字体编辑网站打开一个以 .ttf 格式的字体文件。

清楚的可以看到字体包里面的每一个文字,以 $ 开头的则是 unicode 的缩写了,做了一些处理,否则浏览器会直接解析成对应的字符。

接下来再回头分析 58 同城网页时如何操作的,看见的是 5 则查看源码时看到的则是其他的 unicode 编码,这个 unicode 对应的时另外的生僻的汉字。

先放下这一段,为了更好的理解,把矛头指向阿里图标库使用过阿里图标库的同学应该不是很陌生,阿里图标库会把 svg 文件转换成字体,通过下载引入到我们的项目中,完成图标的展示。针对不同的字体会生成新的 unicode 编码,然而这些新的 unicode 则不会与现有字体库中的 unicode 编码冲突。

就此可以想出 .ttf 文件中的每个一字体都是一个 svg 文件,我们只需要通过技术手段把字体包转换成 svg 获取到 svg 中每个字体的绘制参数,替换掉原有文字的 unicode 替换成生僻字的编码不就可以了吗?说干就干。

程序设计

由于字体文件太多,所以需要在程序运行前要对这些字体文件进行处理操作,转换成 svg 这个过程需要很长的时间,根本就不可能每次接到请求的时候就做这件事情,所以为了能够在请求过程中快速处理,必须在程序运行前就要把字体包转换成svg

但是遇到一个问题,并不是所有文字全部都需要加密的,而是某些特定的字符需要加密,所以,为了保证这个操作,在数据库中写入当前需要加密的字符,在转换成 svg 之后去读取 svg 文件中的标签,把标签中的绘制路径的属性和一些必要参数,根据对应字符存储到数据库当中。

每次接收到数据请求时直接去读取数据库中的数据,然后生成 svg 文件,再把生成好的 svg 文件进行处理成 base64 发送给前端,完成展示。

解析字体文件

遇到的第一个问题就是如何解析字体文件,由于笔者对于其他语言不太数据,所以只能使用 node,通过对npm 仓库的搜索找到了一个相关库ttf2svg

首先安装这个库:

npm install --save-dev ttf2svg

这里使用的基础字体库是 微软雅黑 这个文件在电脑中就可以找到。MicrosoftYaHei.ttf如果找不到的小伙伴可以自行百度一下。

读取字体包转 svg 代码如下:

import path from "path";
import fs from "fs";
import util from "util";

import ttf2svg from "ttf2svg";

//  每次启动前需要删除原有 svg 文件,以防改变了所需要加密的字体包,参数没有及时发生变化
import {removeDir} from "../util/fs";

//  读取文件
const readFile = util.promisify(fs.readFile);
//  写入文件
const writeFile = util.promisify(fs.writeFile);
//  创建文件夹
const mkdir = util.promisify(fs.mkdir);

const ttfToSvg = async () => {
    //  运行根目录
    const rootPath = process.cwd();
    //  .ttf 文件所在目录
    const ttfUrl= path.join(rootPath,"static/ttf/MicrosoftYaHei.ttf");
    //  导出文件文件夹名称
    const saveSvgMkdirName = "ttfSvg";
    //  svg 存储路径
    const saveSvgUrl = path.resolve(rootPath,`${saveSvgMkdirName}/MicrosoftYaHei.svg`);
    //  svg 文件夹路径
    const svgDirUrl = path.resolve(rootPath,saveSvgMkdirName);
    //  读取 ttf 文件生成 buffer
    const ttfBuffer = await readFile(ttfUrl);
    //  通过 ttf2svg 将 buffer 转换成 svg
    const svgContent = ttf2svg(ttfBuffer);
    //  删除原有 svg 文件
    removeDir(svgDirUrl);
    //  创建存放 svg 文件夹
    await mkdir(svgDirUrl);
    //  写入 svg
    await writeFile(saveSvgUrl,svgContent);
    //  返回存放 svg 的路径地址
    return saveSvgUrl;
};

util/fs.js

import fs from "fs";

export const removeDir = (path) => {let files = [];
  if(fs.existsSync(path) ) {files = fs.readdirSync(path);
    files.forEach((file,index) => {
      let curPath = path + "/" + file;
      fs.unlinkSync(curPath);
    });
    fs.rmdirSync(path);
  }
}

生成好所需要的 svg 文件,看看生成好的 svg 是不是我们所需要的呢?

看样子一切都在朝着好的方向发展,这个东西正是我们所需要的。接下来就是开始读取 svg 文件 (这里就不同步数据库了,小伙伴们可以根据自己的需求进行同步处理),想要读取svg 开始的时候还是蛮头疼的,不知道该如何去读取里面的内容。想了想之后觉得 svgxml是差不多的,于是就尝试着使用读取 xml 文件的形式去读取 svg 文件,结果就真的成了。

这里使用 xmldom 来读取的 svg 文件:

npm install --save-dev xmldom

有关 xmldom 的一些文档大家可以自行百度一下,也没有太复杂。具体应用代码如下:

import fs from "fs";
import util from "util";

import {DOMParser} from "xmldom";

const readFile = util.promisify(fs.readFile);

const readSvg = async (svgPath) => {
    //  读取 svg 文件
    const svgContent = await readFile(svgPath);
    //  读取内容转换成 utf8 形式
    const svgHtml = Buffer.from(svgContent).toString("utf8");
    //  生成伪 xml
    const doc = (new DOMParser()).parseFromString(svgHtml, 'application/xml');
    //  获取到第一个 font 标签
    const oFont = doc.getElementsByTagName("font")[0];
    //  获取到 font 下面的所有 glyph 标签,并转换成数组
    //  读取出来的是个伪数组需要转换
    const oGlyphs = Array.from(oFont.getElementsByTagName("glyph"));
    //  测试临时使用数组
    const arr = [];
    //    遍历 oGlyphs 所有标签
    oGlyphs.map((fontEle,index) => {
        //  svg 对应的 unicode
        const unicode = fontEle.getAttribute("unicode");
        //  svg 绘制参数
        const d = fontEle.getAttribute("d");
        //  svg 横向位置
        const horizAdvX = fontEle.getAttribute("horiz-adv-x");
        //  svg 竖向位置
        const vertAdvY = fontEle.getAttribute("vert-adv-y");
        //  这里只是个方便测试做的判断
        if(index === 20 || index === 21 || index === 22) {
          arr.push({unicode,d,horizAdvX,vertAdvY});
        }
    })
    console.log(...arr);
};

执行完上述代码就完成,完全可以读取到里面的所有属性。这个时候忽然感觉已经看到的胜利的曙光有没有,哈哈哈。接下来就是最关键的一步了,如何把读取到的内容转换成是转换成 base64 编码。经过一番搜索之后,找到了 svg2ttf 这个仓库,简直没有太香啊。

安装相关依赖:

npm install --save-dev svg2ttf

具体实现如下:

import fs from "fs";
import util from "util";

import {DOMParser} from "xmldom";

const readFile = util.promisify(fs.readFile);

const readSvg = async (svgPath) => {
    //  读取 svg 文件
    const svgContent = await readFile(svgPath);
    //  读取内容转换成 utf8 形式
    const svgHtml = Buffer.from(svgContent).toString("utf8");
    //  生成伪 xml
    const doc = (new DOMParser()).parseFromString(svgHtml, 'application/xml');
    //  获取到第一个 font 标签
    const oFont = doc.getElementsByTagName("font")[0];
    //  获取到 font 下面的所有 glyph 标签,并转换成数组
    //  读取出来的是个伪数组需要转换
    const oGlyphs = Array.from(oFont.getElementsByTagName("glyph"));
    //  测试临时使用数组
    const arr = [];
    //    遍历 oGlyphs 所有标签
    oGlyphs.map((fontEle,index) => {
        //  svg 对应的 unicode
        const unicode = fontEle.getAttribute("unicode");
        //  svg 绘制参数
        const d = fontEle.getAttribute("d");
        //  svg 横向位置
        const horizAdvX = fontEle.getAttribute("horiz-adv-x");
        //  svg 竖向位置
        const vertAdvY = fontEle.getAttribute("vert-adv-y");
        //  这里只是个方便测试做的判断
        if(index === 20 || index === 21 || index === 22) {
          arr.push({unicode,d,horizAdvX,vertAdvY});
        }
    })

    //  获取 svg 内容
    let svgStr = getSvgStr(arr);
    console.log(svgStr)
    //  把 svg 转换成 ttf
    const ttf = svg2ttf(svgStr,{});
    //  把 ttf 转换成 base64
    const base64 = Buffer.from(ttf.buffer).toString('base64');
    console.log(base64);
};

const getSvgStr = (arr) => {
    //  用与拼接的 svg
    let str = "";
    //  临时替换文件,暂时性的,以后需要替换成所以 unicode
    let _a = ["&#x5539","&#x5535","&#x555C"];
    //  生成 svg 内容
    arr.map((el,index) => {str += `<glyph glyph-name="${+new Date()}"
                      unicode="${_a[index]};"
                      d="${el.d}"
                      horiz-adv-x="${el.horizAdvX}"
                      vert-adv-y="${el.vertAdvY}"/>`;
    })
    //  返回 svg 形式的字符串
    return `<?xml version="1.0" standalone="no"?>
        <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
        <svg xmlns="http://www.w3.org/2000/svg">
        <defs>
          <font id="svgtofont" horiz-adv-x="2688" vert-adv-y="2688">
          <font-face font-family="Microsoft YaHei"
                      font-weight="400"
                      font-stretch="normal"
                      units-per-em="2048"
                      ascent="2167"
                      descent="-536"/>
            <missing-glyph />
            ${str}
          </font>
        </defs>
        </svg>`;
}

这里出了一些小问题,注意我们要把我们所生成的字体 svg 文件的 font 标签部分复制过来,作为参数如果不这样做的话生成的字体会出现位置偏移的现象。

所有工作准备就绪了,执行程序就可以得到应该给前端的 base64 编码了,这我也进行了测试。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<script>
function addStyle (base64,fontName){let oStyle = document.createElement("style");
  oStyle.innerText = `@font-face {font-family: "${fontName}";
                        src: url(data:application/x-font-woff;charset=utf-8;base64,${base64});
                      }`;
  document.head.appendChild(oStyle);
};
const b = "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";
const n = "abc";
addStyle(b,n);
</script>
<body>
<div class="box">
  <div></div>
  <div></div>
  <div style="font-family: abc; color:red; font-size:50px;">&#x5539;&#x5535;&#x555C;567</div>
  <div></div>
  <div class="fiveBox">0123456789</div>
</div>
<script>

</script>
<style>
* {
  margin:0px;
  padding: 0px;
}
.box {
  width:100%;
  white-space: nowrap;
}
.box div {
  width:500px;
  height:50px;
  border:1px solid #ededed;
  /* float: left; */
  font-size: 20px;
  color: pink;
}
.box::after {
  content: "";
  display: block;
  clear: both;
}
</style>
</body>
</html>

以上就是我的测试代码,展示效果如下:

这样下来就和 58 同城的效果是一样的了,经过了几天的调研也算是有了初步的成果,也是有一些成就感的。

总结

总的来说在这个调研的过程中还是学到了很多的东西,比如阿里图标库是如何实现的,字体包里面都有什么等等等。。。虽然在这个过程中用了很多第三方的依赖,但是结果是好的。

文章比较潦草,感谢各位花费这么长时间阅读,文章中如果有什么错误,请在评论处指出,我会尽快做出改正。

正文完
 0