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

对于前端同学来说其实做的更多的事情就是把数据整合好,按照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同城的效果是一样的了,经过了几天的调研也算是有了初步的成果,也是有一些成就感的。

总结

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

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

原文链接:segmentfault.com

上一篇:模拟实现underscore中的防抖(debounce)
下一篇:egg多进程模型注意事项梳理

相关推荐

  • 转行学前端的第 58 天 : 了解 BOM window 弹窗管理

    我是小又又,住在武汉,做了两年新媒体,准备用 6 个月时间转行前端。 今日学习目标 之前主要基于搜索来了解 Javascript 异步管理, 当天其实学的不太明白,之后刚好掘金也停止服务了一天,...

    4 个月前
  • 前端每日实战:58# 视频演示如何用纯 CSS 创作一只美丽的鹦鹉

    效果预览 按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。 https://codepen.io/comehope/pen/vrRmWy 可交互视频 此视频是可以交互的,你可以随...

    2 年前
  • 前端每日实战:158# 视频演示如何用纯 CSS 创作一个雨伞 toggle 控件

    效果预览 按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。 https://codepen.io/comehope/pen/pxLbjv 可交互视频 此视频是可以交互的,你可以随...

    2 年前
  • 【必看】58 道 Vue 常见面试题集锦,涵盖入门到精通,自测 Vue 掌握程度

    1.vue优点?答:轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb ;简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;双向数据绑定:保留了 angular 的特...

    4 个月前
  • uni-app自定义导航icon字体库的问题

    官方文档: 一开始不是很理解,因为在这个平台 iconfont 生成的unicode代码是&amp;#xe604;这样的,根本用不了,不断测试下用把&amp;#x改成\u就可以了 1.首页在icon...

    2 年前
  • bs58check

    A straightforward implementation of base58-check encoding bs58check A straight forward implement...

    2 年前
  • bs58

    Base 58 encoding / decoding bs58 JavaScript component to compute base 58 encoding. This encoding is...

    2 年前
  • base-58

    Base58 encoding &amp; decoding for Node.js &amp; web agent windows. Base58 in a window Get Base58.js...

    1 年前
  • @types/bs58

    TypeScript definitions for bs58 Installation npm install --save @types/bs58 Summary This package c...

    7 个月前
  • 5858快到家 React+hooks+redux项目实战

    前言 哔————传送门 项目地址 github地址 ​ 秋招正当时,没有一个拿得出手的React实战项目怎么能行?笔者最近恰好读到了神三元大佬在掘金的React Hooks 与 I...

    2 个月前

官方社区

扫码加入 JavaScript 社区