关于外部信息采集
前言
本文将从个人角度分享一些外部信息采集的方法以及技术理解,并提出一些个人认为合理的防范措施,希望能提供帮助。
本文主要分为四部分,其一为一些数据采集方法介绍,会从API、WEB、APP三种应用场景逐一分享;其二为一些数据采集工具的分享;其三为简略的采集服务调度模型分享;其四为数据采集防范的相关看法和经验分享。
一、数据采集方法
A ) 从API接口进行采集
描述
场景:API服务对接
特点:
可预见,变化少,开发门槛低
使用量有明文要求,错误有明显提示,维护成本低
数据可靠性高
使用方式:HTTP请求构造
示例
API文档截图
请求指令
curl -H "Authorization: Bearer XXX" "https://api.appannie.com/v1.3/apps/google-play/app/XXXXXX/ratings?page_index=0"结果
{"ratings": [{"country": "WW", "current_ratings": null, "all_ratings": {"star_1_count": 334019, "average": 3.88, "star_5_count": 1106111, "rating_count": 1921701, "star_2_count": 91834, "star_4_count": 238298, "star_3_count": 151439}}], "page_index": 0, "code": 200, "prev_page": null, "next_page": null, "page_num": 1, "product_name": "Paper.io 2"}
B ) 从WEB内容进行采集
其一:HTTP请求构造
描述
场景:目标数据为WEB内容(通常通过浏览器的开发者工具即可直接获得的数据)
特点:
难点在请求内容解析,构造相对简单
不可预见,变动情况不定,返回内容多样,高度定制化
使用量无明文要求,错误需要自行发现和判断,维护成本高
数据可靠性不定
使用方式:HTTP请求分析和构造
示例
假设我们要获取“天气”关键字的搜索联想词信息
通过浏览器可以发现请求格式大致如下
curl 'https://www.baidu.com/sugrec?pre=1&p=3&ie=utf-8&json=1&prod=pc&from=pc_web&sugsid=34267,34099,34225,31660,34277,33848,34092,34111,26350&wd=%E5%A4%A9%E6%B0%94&req=2&bs=%E5%A4%A9%E6%B0%94&csor=2&cb=jQuery110206309653347430035_1626857712796&_=1626857712797' \-H 'Connection: keep-alive' \-H 'Pragma: no-cache' \-H 'Cache-Control: no-cache' \-H 'sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"' \-H 'Accept: text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01' \-H 'X-Requested-With: XMLHttpRequest' \-H 'sec-ch-ua-mobile: ?0' \-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36' \-H 'Sec-Fetch-Site: same-origin' \-H 'Sec-Fetch-Mode: cors' \-H 'Sec-Fetch-Dest: empty' \-H 'Referer: https://www.baidu.com/s?wd=xxxxxx' \-H 'Accept-Language: zh-CN,zh;q=0.9' \-H 'Cookie: xxxxx' \--compressed
获得结果
{"q":"天气","p":false,"g":[{"type":"sug","sa":"s_1","q":"天气预报"},{"type":"sug","sa":"s_2","q":"天气预报郑州"},{"type":"sug","sa":"s_3","q":"天气预报15天查询"},{"type":"sug","sa":"s_4","q":"天气郑州"},{"type":"sug","sa":"s_5","q":"天气预报河南"},{"type":"sug","sa":"s_6","q":"天气预报24小时详情"},{"type":"sug","sa":"s_7","q":"天气河南"},{"type":"sug","sa":"s_8","q":"天气预报下载2021最新版免费"},{"type":"sug","sa":"s_9","q":"天气热皮肤起小红疙瘩特别痒"},{"type":"sug","sa":"s_10","q":"天气太热怎么发朋友圈"}],"slid":"1872835131631399082","queryid":"0x98a5a127c2f0aa"}
局限
格式多样,可能需要做HTML、XML等内容解析
有状态的请求前后,需要维护cookie
难以实现页面加载和运行动作(例如处理验证码、获取JS计算结果等),有些页面还会通过接口返回JS内容,然后再通过浏览器执行JS来获取数据
页面上的媒体资源(例如图片)无法直接获取,需要先解析出链接地址,然后构造请求进行获取
其二:浏览器模拟操作
描述
场景:目标数据为WEB内容(无法通过构造请求直接获得的数据),通过模拟用户操作浏览器获取
特点:
需要借助三方工具,本质是模拟用户操作进行自动化测试
不需构造请求,但需要明确用户操作流程
仅能获取页面运行后的结果,如果需要获取过程中产生的具体请求内容,需要借助代理工具
使用方式:通过浏览器驱动操作浏览器以获取页面内容
示例
以python使用selenium + geckodriver + firefox进行数据采集为例
仅获取页面结果
封装一个selenium的firefox驱动的Firefox类
# -*- coding: utf-8 -*-import timefrom selenium import webdriverclass Firefox():driver = Nonedef __init__(self):# 浏览器设置profile = webdriver.FirefoxProfile()profile.set_preference("dom.webdriver.enabled", False)profile.set_preference("accept_untrusted_certs", True)self.profile = profile# 启动参数设置options = webdriver.FirefoxOptions()options.add_argument("--disable-infobars")options.add_argument("--disable-extensions")options.add_argument('--no-sandbox')# 无界面模式,可以加快运行效率,linux服务器无界面情况必选。开发时可以不选,这里为了演示先关闭# options.add_argument('--headless')# 忽略证书错误,以避免https安全警告options.add_argument('--ignore-certificate-errors')self.options = optionsdef set_proxy(self, proxy):self.profile.set_proxy(proxy)def start(self):# 注意必需先安装好firefox浏览器# 驱动地址driver_path = "./tools/geckodriver.exe"# 启动浏览器self.driver = webdriver.Firefox(firefox_profile=self.profile, executable_path=driver_path,firefox_options=self.options)def stop(self):self.driver.quit()
调用上方Firefox类请求咱们公司官网
if __name__ == '__main__':# 启动浏览器驱动firefox = Firefox()firefox.start()# 访问公司介绍页firefox.driver.get("https://www.37wan.net/introduction.html#company")# 简单处理,等待一下加载完成,实际使用时可以通过观察页面目标元素是否已加载出来来判断time.sleep(10)print(firefox.driver.page_source)# 退出浏览器firefox.stop()
浏览器驱动运行后,打开浏览器并访问了我们的目标网址
代码运行的结果如下
代理获取具体请求
继续沿用上面的Firefox类,新增继承browsermobproxy的Server类的ProxyServer类,用来加入我们的配置
from browsermobproxy import Serverclass ProxyServer(Server):def __init__(self):# 屏蔽父类构造函数# super(ProxyServer, self).__init__()self.process = Noneself.proxy = Noneself.selenium_proxy = None# 配置browsermob-proxyself.java_path = "java"self.path = "./tools/browsermob-proxy-2.1.4/bin/browsermob-proxy"self.proxy_dir = "/tools/browsermob-proxy-2.1.4"self.proxy_jar_path = "./tools/browsermob-proxy-2.1.4/lib/browsermob-dist-2.1.4.jar"self.host = "localhost"self.port = 3737self.command = []self.command += [self.java_path,"-Dapp.name=browsermob-proxy","-Dbasedir=%s" % self.proxy_dir,"-jar",self.proxy_jar_path,'--port=%s' % self.port]def get_process_pid(self):return self.process.piddef start(self, options=None):super(ProxyServer, self).start(options=options)self.proxy = self.create_proxy()self.selenium_proxy = self.proxy.selenium_proxy()def new_har(self):# 代理选项,截取http头、内容options = {'captureHeaders': True, 'captureContent': True, 'captureBinaryContent': True}self.proxy.new_har("har", options=options)
我们在浏览器驱动中设置好代理,然后来获取我们官网页面中的公司发展史接口数据
if __name__ == '__main__':# 创建代理local_proxy = ProxyServer()local_proxy.start()# 实例化浏览器驱动firefox = Firefox()# 给浏览器设置好代理firefox.set_proxy(local_proxy.selenium_proxy)# 启动浏览器firefox.start()driver = firefox.driver# 重置请求记录local_proxy.new_har()# 加载页面driver.get("https://www.37wan.net/introduction.html#company")time.sleep(5)# 此次页面加载执行的所有请求记录entries = local_proxy.proxy.har['log']["entries"]# 遍历所有请求记录,通过目标接口地址筛选出我们要的数据for entry in entries:if str(entry['request']['url']).find("https://www.37wan.net/api/web/about/history") != -1:print(entry)break# 退出浏览器firefox.stop()# 退出代理local_proxy.stop()
获取回来的请求记录数据格式如下
{"pageref": "har","startedDateTime": "2021-07-21T17:56:41.072+08:00","request": {"method": "GET","url": "https://www.37wan.net/api/web/about/history","httpVersion": "HTTP/1.1","cookies": [],"headers": [{"name": "Host","value": "www.37wan.net"},{"name": "User-Agent","value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"},{"name": "Accept","value": "*/*"},{"name": "Accept-Language","value": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2"},{"name": "Accept-Encoding","value": "gzip, deflate, br"},{"name": "X-Requested-With","value": "XMLHttpRequest"},{"name": "Connection","value": "keep-alive"},{"name": "Referer","value": "https://www.37wan.net/introduction.html"}],"queryString": [],"headersSize": 389,"bodySize": 0,"comment": ""},"response": {"status": 200,"statusText": "OK","httpVersion": "HTTP/1.1","cookies": [],"headers": [{"name": "Date","value": "Wed, 21 Jul 2021 09:56:35 GMT"},{"name": "Content-Type","value": "application/json; charset=utf-8"},{"name": "Transfer-Encoding","value": "chunked"},{"name": "Connection","value": "keep-alive"},{"name": "Server","value": "nginx"},{"name": "Cache-Control","value": "no-cache"},{"name": "Pragma","value": "no-cache"},{"name": "Cache-Control","value": "no-cache"},{"name": "Cache-Control","value": "max-age=0"},{"name": "X-Via","value": "1.1 PSzjnbsxsy229:9 (Cdn Cache Server V2.0), 1.1 uzhoudianxin65:12 (Cdn Cache Server V2.0),1.1 mdx76:13 (Cdn Cache Server V2.0)"},{"name": "X-Ws-Request-Id","value": "60f7ef53_PS-000-01QTd185_18413-23653"}],"content": {"size": 17238,"mimeType": "application/json; charset=utf-8","text": "《这里是具体请求的返回内容,太长这里就不显示了》","comment": ""},"redirectURL": "","headersSize": 458,"bodySize": 17238,"comment": ""},"cache": {},"timings": {"comment": "","connect": -1,"send": 0,"receive": 2,"blocked": -1,"wait": 164,"ssl": -1,"dns": -1},"serverIPAddress": "113.96.140.117","comment": "","time": 166}
局限
单浏览器进行的操作是串行的,并行操作需要多开浏览器,并发处理效率不高
对三方工具依赖性很强,使用时必需做好异常捕获、报警、自我恢复,以及时发现和处理问题
服务器上运行时没有图形化界面,需要借助代码调用浏览器驱动进行截图来debug
C ) 从APP应用进行采集
其一:HTTP请求构造
描述
场景:目标数据为APP应用数据(可解析并构造请求的数据)
特点:
难点在请求内容解析,构造相对简单
需要通过代理截取请求进行解析,HTTPS请求需要想办法破解
使用量无明文要求、错误需要自行发现和判断,维护成本高
数据可靠性不定、请求和返回格式不可预见、变动情况大致与APP版本变动节奏一致
使用方式:通过代理截取(或补充破解https)分析APP请求内容,再进行请求构造
示例
这里以应用宝APP的预约列表为例
先通过Charles代理,直接获取代理请求内容进行分析,发现是乱码。
在Charles设置解析目标域名下的内容
再次访问发现请求失败
接下来我们给模拟器加装Charles的证书
重新访问,发现请求内容已经解析出来
请求解析完成后,接下来只需要和之前一样进行请求构造获取数据即可,此处不继续演示。
局限
HTTPS破解门槛较高,限制较多
安卓7.0后多数只信任系统自带证书,如果修改需要root权限,这就要求APP能在root环境下运行。安卓5.0虽无证书限制,但是要求APP兼容。即使满足前面两点,如果APP有证书加固,或者有内置加密方式,则依旧无法解出密文获取内容(除非破解源码)。
如果有通过APP操作设置请求状态的行为,则难以实现破解
其二:手机模拟操作
描述
场景:目标数据为APP内布局节点文字数据,通过模拟用户操作手机获取
特点:
重点在手机调度、APP操作及节点解析上
仅能通过节点解析获取文字内容,无法获取媒体资源(但可以操作截图)
需要使用硬件,或者使用云手机服务,维护成本较高
数据可靠性不定、变动情况大致与APP版本变动节奏一致
使用方式:本质是通过Airtest(基于adb调试)进行自动化测试,操作手机以获取数据
示例
这里使用华为云手机进行示例演示。我们先启动一个连接远程虚拟手机的ssh链接
通过airtest查看设备
示例代码
# -*- encoding=utf8 -*-from airtest.core.api import *if __name__ == '__main__':target_link = "android:///127.0.0.1:3737"connect_device(target_link)from poco.drivers.android.uiautomation import AndroidUiautomationPocopc = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False)list_view = pc("android:id/content").child("android.widget.RelativeLayout").offspring("com.vivo.game:id/list_view").child("android.widget.LinearLayout")for item in list_view:try:title = item.offspring("com.vivo.game:id/game_common_title").get_text()time = item.offspring("com.vivo.game:id/game_publish_time").get_text()number = item.offspring("com.vivo.game:id/game_appointment_number").get_text()except:continueprint(title, time, number)break
运行结果
二、一些工具分享
Selenium
地址:https://www.selenium.dev/
介绍:Selenium 是一系列工具和库的综合项目,这些工具和库支持 web 浏览器的自动化。
BrowserMobProxy
地址:https://github.com/lightbody/browsermob-proxy/releases
介绍:BrowserMobProxy会提供一个ProxyServer用于做转发代理拦截,这个server可以是单独部署,也可以编码进代码中。
airtest
地址:https://airtest.netease.com/
介绍:网易游戏出品的一个移动端自动化测试框架
Charles
地址:https://www.charlesproxy.com/
介绍:代理和抓包工具
三、简单的采集调度模型
结构设计
此模型采用多线程方式进行工作,主要分为三层,有五个模块。
三层
第一层:数据源管理层
第二层:任务调度层
第三层:任务和请求处理层
五模块
模块1:数据源采集线程(第一层)
模块2:Generator(第二层)
模块3:Manager(第二层)
模块4:Worker(第三层)
模块5:Request(第三层)
运行说明
主线程依据需要,启动对应数量的【数据源采集线程】,维护这些线程的运行状态
【数据源采集线程】线程启动一个任务管理【Manager】线程,同时可以按需启动一个【Generator】任务生成线程来自动产生任务
【Manager】线程定时扫描任务记录,当有任务需要被启动时,启动该任务对应的【Worker】任务处理线程,开始处理任务
【Worker】线程为任务处理最小执行单位,一个【Worker】线程对应一个任务记录。【Worker】线程依据任务生成时指定的参数配置,组装并生成对应数量的请求记录,并负责调度这些请求,启动【Request】线程并交付其处理。【Worker】还将在在请求完成后进行过滤和回调。
【Request】请求处理线程为请求处理最小执行单位,一个【Request】线程对应一个请求记录。【Request】会依据自身参数配置(url、header、get、post)执行请求,过滤结果并写入ETL处理队列。
一个任务对应【Worker】下所有请求【Request】处理完成后,【Worker】会进入“任务结束”流程,处理回调逻辑,并更新任务状态。
至此【Worker】与【Request】生命周期往复循环,【数据源采集线程】【Generator】【Manager】常驻并提供服务
四、数据采集防范
防范请求伪造
使用带验证信息的请求,同时加强验证信息设置的安全性
比如请求必须验证登录态,同时登录必须做好验证,防止自动化操作。在目前图像文字提取的算法已经普及的情况下,需要提高攻击者的门槛,比如使用图片含义识别可以在校验方面提高门槛,而使用拖拽方式完成校验可以提高操作门槛。
内容加密,同时用户端定时更换加密算法
内容加密,可以防止攻击者直接通过解析http协议获取内容。此外,对于web端来说,虽然前端源码可能有混淆,但依旧有被找出加密算法的风险,所以定时更换加密密钥没有直接更换加密算法可靠。通过频繁更换算法,可以提高攻击者需要人为介入的频次,从而使其长期的采集失去意义。
对于WEB应用,可以通过JS执行计算出页面内容
请求构造只能拿到请求内容,如果页面不直接使用请求结果,而是通过JS计算出来,也可以提高攻击门槛,因为无论是解析JS算法或是手动执行JS代码都需要更繁琐的操作。
防范模拟操作
对于重要数据做访问验证,在验证时尽量使用自动化门槛更高的校验方式
如果是模拟手机操作,除了常见的校验,还可以通过手机号校验或者其他硬件功能来进行校验,例如摇一摇、语音验证等,对于攻击者来说门槛更高。
对于web应用,可以尝试在不影响页面呈现的情况下随机变换节点布局和名称
对于攻击者,拿到返回内容后自然需要解析,而对于HTML内容解析,我们可以采取随机变换布局的方式进行反制。通常为了解析HTML,我们需要分析目标元素所在的节点,然后通过class、id或标签等元素进行相对或绝对路径定位。如果这些名称、标签或相对位置可以随机变换的话,对于想做精确解析的攻击者来说无疑是很棘手的。
特征识别
对于模拟操作,通常都会有一些特点存在,例如模拟手机操作的airtest是基于adb调试,那么进行手机模拟操作就必须打开调试模式;对于浏览器自动化操作的selenium,会有一些内置的JS属性(例如window.navigator.webdriver)。
除此之外也可以考虑统计分析用户访问频次或者操作日志,然后划分风险等级进行管控。