算法能力
思想是代码的灵魂,一个开发人员总是需要或多或少掌握一些算法的,特别是在网络安全领域。
例如,在实现web应用扫描器的时候,我们需要实现一个web2.0的整站爬虫,就需要用到优先级广度优先搜索算法来调用爬虫对页面的抓取过程,同时也需要页面结构相似度比较算法来去除相似结构的页面和利用布隆过滤器对爬过的链接进行去重过滤。
下面,我就简单描述一下这几个算法
页面结构相似度计算方法
以下面的页面为例
现目前分别有hyTest和chaojilajiTest这两个子页面,http://47.101.39.152/knowledge/network?id=1 和 http://47.101.39.152/knowledge/network?id=2
如果不进行页面相似度分析的话,普通爬虫就会对这两个页面进行抓取和解析,那么就会造成资源的浪费。
那么首先,我们需要对url的结构进行分析,对http协议的url进行拆解,可以拆解为
http(s)://hostname:port/path?query
而我们需要关注的点主要是query和path,如果发现有数字,时间等值在这些参数里面,那么就极有可能是结构相似的。
以上面的链接为例:
http://47.101.39.152/knowledge/network?id=1
http://47.101.39.152/knowledge/network?id=2
很明显,这里的相似出现在query部分的id中,那么我们就可以记录一个正则模式
http://47\.101\.39\.152/knowledge/network\?id=(\d+)
那如果有些界面光从链接上看不出来规律咋办呢?
这时我们就可以考虑在抓取页面的过程中去维护页面结构,即先抓取,再处理规律。
如何定义页面结构相似性?先来看看页面A
利用无头浏览器的方式可以抓取到完整的页面(不管是如何复杂的单页应用都可以抓取),抓取方式不在本文的范围内,有需求的朋友可以搜我以前写过的博客。
那么整理一下,获取到的结构如下:
#document-html-head-style-style-meta-link-meta-meta-link-link-title-link-link-style-script-script-link-script-script-script-script-script-link-script-link-script-script-script-link-script-style-body-svg-symbol-path-path-path-path-path-path-path-symbol-path-path-symbol-path-path-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-symbol-path-path-path-path-symbol-path-path-path-path-path-symbol-path-path-path-path-path-path-path-path-symbol-path-path-path-path-path-path-path-symbol-path-path-path-symbol-path-path-path-symbol-path-path-path-path-path-symbol-path-path-symbol-path-path-path-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-svg-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-path-path-symbol-path-path-path-symbol-path-symbol-path-path-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-symbol-path-div-div-div-div-div-img-button-span-div-button-span-svg-use-span-button-span-svg-use-span-button-span-svg-path-span-div-div-div-div-div-button-span-svg-use-span-span-span-span-svg-use-input-span-span-svg-path-div-div-div-div-div-div-div-table-colgroup-col-col-col-col-col-thead-tr-th-th-th-div-span-span-span-span-svg-path-span-svg-path-th-div-span-span-span-span-svg-path-span-svg-path-th-tbody-tr-td-div-td-div-td-div-td-div-td-div-tr-td-td-div-div-div-div-div-td-td-td-div-button-span-button-span-tr-td-td-div-div-div-div-div-td-td-td-div-button-span-button-span-ul-li-li-button-span-svg-path-li-a-li-button-span-svg-path-script-script-script-style-div-div-div
分析程序如下:
public static List<String> getAllLabelsFromHtml(String html) {
Document document = Jsoup.parse(html);
Elements elements = document.getAllElements();
List<String> elementList = new ArrayList<>();
for (Element element : elements) {
elementList.add(element.nodeName());
}
return elementList;
}
再来看看页面B
获取到的页面结构序列如下:
#document-html-head-style-style-meta-link-meta-meta-link-link-title-link-link-style-script-script-link-script-script-script-script-script-link-script-link-script-script-script-link-script-style-script-link-script-body-svg-symbol-path-path-path-path-path-path-path-symbol-path-path-symbol-path-path-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-symbol-path-path-path-path-symbol-path-path-path-path-path-symbol-path-path-path-path-path-path-path-path-symbol-path-path-path-path-path-path-path-symbol-path-path-path-symbol-path-path-path-symbol-path-path-path-path-path-symbol-path-path-symbol-path-path-path-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-path-path-symbol-path-path-path-svg-symbol-path-path-path-symbol-path-path-path-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-path-path-symbol-path-path-path-symbol-path-symbol-path-path-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-symbol-path-symbol-path-symbol-path-path-symbol-path-symbol-path-symbol-path-div-div-div-div-div-div-img-button-span-div-div-div-div-button-span-svg-use-span-button-span-svg-use-span-button-span-svg-path-span-div-div-ul-li-span-svg-use-span-li-div-span-svg-use-span-i-ul-li-span-li-span-svg-use-span-li-span-svg-use-span-div-span-span-svg-path-div-div-div-div-div-div-div-img-p-span-div-div-div-div-div-div-div-div-div-div-div-div-button-span-svg-path-div-div-div-div-div-div-div-div-h2-div-p-div-div-span-input-span-span-span-svg-path-p-div-p-div-div-div-div-h2-div-p-span-svg-path-path-div-div-div-div-div-div-div-div-div-div-p-span-svg-path-path-div-div-div-div-div-div-div-div-div-div-span-svg-path-div-button-span-button-span-div-div-div-div-span-span-span-svg-use-input-span-span-svg-path-span-img-div-div-div-div-div-div-ul-li-button-span-svg-path-li-a-li-button-span-svg-path-div-img-p-div-div-div-div-div-span-svg-path-div-div-img-p-p-div-span-input-span-span-svg-path-button-span-svg-use-span-div-div-img-p-p-div-div-img-div-div-img-div-div-div-div-button-span-svg-path-span-div-div-div-div-div-div-span-svg-use-span-span-span-span-div-div-span-svg-path-span-div-div-div-span-svg-use-div-span-svg-use-div-div-div-div-canvas-div-div-div-span-svg-use-div-div-span-svg-use-div-div-span-svg-use-div-div-span-svg-use-div-script-script-script-style-div-div-div-div-div-div-div-div-div-button-span-span-svg-path-div-div-div-div-div-div-div-div-div-span-span-svg-use-span-div-div-div-div-svg-g-g-g-g-div-div-div-div-div-div-div-div-img-div-div-div-div-div-div-div-div-button-span-span-svg-path-div-div-div-div-div-div-div-div-div-span-span-svg-use-span-div-div-div-div-svg-g-g-g-g-div-div-div-div-div-div-div-div-img-div-div
那么,现在我们就可以利用这两个序列计算得到公共长度,从而得到相对相似度。
代码如下:
public class SequenceUtils {
/**
* 获取公共长度(可打断,保留顺序。总长度)
* 算法思想:动态规划
* 普适转移方程:
* 涩 dp[i][j] 表示 长度为 i的a序列和长度为j的b序列的的公共长度
* 如果 a[i] == b[j] ,则 dp[i][j] = max(dp[i-1][j-1]+1,max(dp[i-1][j],dp[i][j-1]))
* 如果 a[i] != b[j] ,则 dp[i][j] = max(dp[i-1][j],dp[i][j-1])
*
* @param a 序列a
* @param b 序列b
* @return dp[a.size()-1][b.size()-1]
*/
public static Integer getLongestCommonSequence(List<String> a, List<String> b) {
int n = a.size();
int m = b.size();
int[][] dp = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (i == 0 && j == 0) {
if (a.get(i).equals(b.get(j))) {
dp[0][0] = 1;
} else {
dp[0][0] = 0;
}
} else {
if (i == 0 && j != 0) {
if (a.get(i).equals(b.get(j))) {
dp[0][j] = dp[0][j - 1] + 1;
} else {
dp[0][j] = dp[0][j - 1];
}
} else if (i != 0 && j == 0) {
if (a.get(i).equals(b.get(j))) {
dp[i][0] = dp[i - 1][0] + 1;
} else {
dp[i][0] = dp[i - 1][0];
}
} else {
if (a.get(i).equals(b.get(j))) {
int tmp = dp[i - 1][j - 1] + 1;
dp[i][j] = Math.max(tmp, Math.max(dp[i - 1][j], dp[i][j - 1]));
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
}
}
return dp[n - 1][m - 1];
}
public static Double pageStructScore(List<String> a, List<String> base) {
Integer length = getLongestCommonSequence(a, base);
int n = base.size();
int m = a.size();
// TODO: 2020/5/25 定义:页面结构的相似度为 (2.0*公共序列的长度)/(旧的公共序列的长度+新的公共序列的长度)
Double score = (2 * length) / ((n + m) * 1.0);
return score;
}
}
这是一个动态规划算法的实践。
A的长度为511 B的长度为754 公共序列的长度为464
0.733596837944664
然后我们只需要自己定义一个阈值,来设定一下达到多少的相似度可以认为是两个相似的页面即可。
那么对于相似的页面,我们再来分析他们url的相似性,就可以分析出新的url相似关系,从而减少抓取量。
分布式爬虫调度算法-优先级广度优先搜索算法
我们可以使用playwright或者puppeteer来进行web2.0的页面抓取,但是这些页面抓取框架并没有为我们提供页面抓取顺序的方案,所以我们就需要根据自己的理解实现这么一套内容。
大概的思路如下:我们将一个网站的链接定义为一个个节点,那么整个网站就是一张图,那么我们从首页,利用广度优先的方式一层一层的将链接发现出来,理论上讲就可以遍历整个网站(不考虑孤岛页面)。而且,根据这样的方式,我们也是先把重要的页面先抓取了(最靠近首页的)。同时,我们利用给链接赋予优先级的方式,可以更好的优化遍历过程,所以需要将广度优先搜索的普通队列,替换为优先队列。
在抓取的过程中,由于我们的程序要解决避免重复抓取和并行抓取的矛盾点,同时减少加锁的开销,所以需要对这个过程进行优化。我最终实践的结果是全程同步执行,逐层并发执行。
即对于扩展出来的每一层链接进行统一管理,然后对这一整层的链接进行并发抓取。部分源码如下:
//nextfloorset中保存了当前层的下一层需要爬取的url,并全部添加到队列里面
while (!queue.isEmpty()) {
if (task.isNeedDelete()){
// TODO: 2019/9/25 项目待删除
return;
}
Object object = queue.element();
if (object instanceof UrlNode) {
UrlNode urlNodex = (UrlNode) object;
if (webProcessInfo.getAccessedUrls().contains(urlNodex.getUrl())) {
((LinkedList) queue).pop();
continue;
}
if (urlNodex.getDepth() > webProcessInfo.getHeight()) {
queue.clear();
break;
}
if (webProcessInfo.getAccessedUrls().size() >= webProcessInfo.getNumber()) break;
if (currentDepth < urlNodex.getDepth()) {
//说明是下一层的url节点了,开始执行该层的节点爬取即可
//并发获取页面并获取页面的子链接
Set<String> uncheckedUrlSet = new HashSet<>();
ExecutorService pool = Executors.newFixedThreadPool(webProcessInfo.getMaxThread());
List<Callable<Integer>> callers = new ArrayList<>();
for (String linkurl : nextfloorset) {
callers.add(() -> {
crawlParseService.getChildUrls(linkurl, baseUrl, webProcessInfo, uncheckedUrlSet, task.getLogId(), task.getSiteId());
return null;
});
}
try {
pool.invokeAll(callers);
pool.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
//将爬取后的url放到已访问的url集合中
webProcessInfo.addAllUrl(nextfloorset);
if (webProcessInfo.getAccessedUrls().size() >= webProcessInfo.getNumber()) {
queue.clear();//主动回收内存空间
break;
}
//维护每层之间的变量
nextfloorset.clear();
currentDepth += 1;
//如果当前深度等于最大深度,直接退出遍历
if (currentDepth == webProcessInfo.getHeight()) {
queue.clear();//主动回收内存空间
break;
}
for (String uncheckedUrl : uncheckedUrlSet) {
int accessSize = webProcessInfo.getAccessedUrls().size();
if (webProcessInfo.getAccessedUrls().contains(uncheckedUrl))
continue;//多加一个判断:因为找子节点在加入父节点前面
if ((accessSize + nextfloorset.size()) >= webProcessInfo.getNumber()) {
break;
}
//维护下一层待执行的url 的set nextfloorset,并把它们都加入到队列中
nextfloorset.add(uncheckedUrl);
UrlNode urlNode1 = new UrlNode();
urlNode1.setDepth(currentDepth + 1);
urlNode1.setUrl(uncheckedUrl);
((LinkedList) queue).offer(urlNode1);
}
uncheckedUrlSet.clear();
((LinkedList) queue).pop();//从队列中去掉当前这个
}
}
}
其中这里的
ExecutorService pool = Executors.newFixedThreadPool(webProcessInfo.getMaxThread());
List<Callable<Integer>> callers = new ArrayList<>();
for (String linkurl : nextfloorset) {
callers.add(() -> {
crawlParseService.getChildUrls(linkurl, baseUrl, webProcessInfo, uncheckedUrlSet, task.getLogId(), task.getSiteId());
return null;
});
}
try {
pool.invokeAll(callers);
pool.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
就是做了逐层并发抓取的过程。
布隆过滤器
布隆过滤器的介绍,由于不是我自己创造的,所以看看别人写的好点的吧
总结
互联网上安全开发的资料不多,很多时候查资料只能查个大概,核心的东西还是得自己摸索,所以,具备一定的数据结构和算法的能力是必不可少的。目前我工作中使用到的顺丰有如下几种:
1、动态规划
2、树的遍历,图的遍历
3、字典树
4、字符串相关算法(正则表达式也是一种算法)
等等
计算机网络
既然我们做的是安全开发,那么网络肯定是必不可少的技术点
首先,最基本的,你了解HTTP协议请求过程吗?
了解HTTP协议
首先,我们来看看HTTP协议请求过程。
当一个HTTP请求从浏览器处产生之后,浏览器首先会读取本地有没有该请求的缓存,如果有,浏览器将直接返回缓存中的结果,所以有时候我们在开发的过程中需要强制刷新浏览器清除浏览器缓存就是避免浏览器读取以前的结果造成误差。
在去除了缓存之后,首先浏览器去去读取本地Hosts文件里面的ip域名映射关系,如果找到了,那么浏览器会立即向该ip发出请求(一般是80端口)。如果没有找到,则会像最近的DNS域名服务器发起查询请求,DNS域名查询系统是一个递归查询的过程,递归中伴有迭代。在查询到该域名对应的IP地址之后,浏览器就会向该ip发出请求。请求发出之后,经过各层的封装和解析,最终通过路由跳转可以到达目标服务器。目标服务器很有可能是一个nginx反向代理,那么此时nginx就会根据配置以及http请求携带的信息来转发到对应的服务中。
当后端程序接收到http请求之后,会将http请求反序列化成相应的程序语言,以java为例,servlet会将整个请求的链接部分,参数部分,请求头部分和body部分都解析成对应的java实例。
根据路由,不断的向上层业务进行分发,直到分发到最底部的业务层,等到处理完毕之后,通过框架层的封装,将结果通过进入路径重新走回去。
HTTP协议中敏感的响应头
未完,待续