继之前爬了几个 CT 在线报告站之后,又遇到了一个新的网站,同样是别人的报告找我来爬。
之前的:
该报告的链接格式为https://ylyyx.shdc.org.cn/#/home?sid=xxx&token=xxx&appid=SHPulmH
,上去后是一个手机端的页面,而且十分简单连设置菜单都没有,图我就不放了。
根据域名能搜到这个网站是上海申康医院发展中心的,有 37 家医院用它的系统。
先看请求吧(由于不是我的报告所以打码了),这个站的请求倒是简单,下面那一堆就是影像文件,转存下来用记事本打开一眼瞄到到 DICM 字样,前头有 128 字节的空白,可以确定。
但是在请求头里发现有 Authorization,删掉重发返回 401,也就是说该站有前端加密,得先把它破了才能爬。
最后呢经过两小时的研究,成功搞定了该网站,总的来说本站的 API 签名属于最简单的那类,估计只是为了应付合规要求,非常适合作为爬虫的入门练习。
本文对应的代码开源 https://github.com/Kaciras/cloud-dicom-downloader。
为了跟之前的代码保持一致,我用了 Python 写爬虫,但实际上这并不是最佳选择,如果做成浏览器插件的话会简单许多,可以做交互式操作,比如过认证部分让用户来搞。
本文的破解方案如果网站更新改了加密方式就会失效,未来本下载器可能要重写成插件。
除了刚才的请求,还发现前头的 API 请求也有校验,参数里的sid
和token
在 URL 的参数里、mode
固定是 0;然而nonc_str
和sign
在页面、URL、Cookies 中都找不到,猜测是计算后生成的。
首先搜nonc_str
,在源码里能定位到相关代码:
可以看到nonc_str
是变量s
,最上面有s=u(6)
,u
这个函数就是从它那一串字(Base62)中随机选几个拼接,所以咱也直接随机生成就好。
接下来整个请求参数传给了函数c
,来看看c
的代码:
首先它把请求参数序列化为 URL 的参数格式,即key=value&key2=...
这样,键值对按照 JS 的for-in
顺序,在现代浏览器中对象的属性如果只包含纯英文字母,那遍历顺序就是添加的顺序。
然后又在生成好的字符串尾部加了一个key
参数,值为变量o
,o
在源码1的最上面等于m()
,m
这个函数呢就是用 AES 解密了一个固定的字符串。
在m
内下断点跟进AES.decrypt
发现它所在的文件叫crypto-js.<hash>.js
,看来是调的 crypto-js,咱也装上这个包然后运行下函数m
,解密出字符串5fbcVzmBJNUsw53#
,这就是key
参数的值。
最后回到函数c
,它把拼接好的字符串做个 MD5,得到的结果转 HEX 作为sign
参数。对应的 Python 代码:
import hashlib
import random
import re
import string
from urllib.parse import urlencode
TABLE_62 = string.digits + string.ascii_lowercase + string.ascii_uppercase
KEY = "5fbcVzmBJNUsw53#"
NONCE = "".join(random.choices(TABLE_62, k=6))
def _sign_parameters(query: dict, params: dict):
"""
该网站的 API 请求有签名机制,算法倒不复杂,扒下代码就能还原。
:param query URL 中的参数
:param params API 请求的参数,签名会添加到上面
"""
params["nonce_str"] = NONCE
params["token"] = query["token"]
input_ = urlencode(params) + "&key=" + KEY
params["sign"] = hashlib.md5(input_.encode()).hexdigest()
搞定签名接下来就可以抓取列表了,发送 GET 到/api001/series/list
可以拿到序列的列表,result
字段就是所有的序列,由其可以拼出图像请求的路径:
/rawdata/indata/
。source_folder
值。names
里以逗号分隔。接下来就是最开始的计算认证头,搜Authorization
直接就定位到了发请求的代码:
此处变量f
由 4 个部分加分号作为分隔拼成,前两个还是sid
跟token
都在 URL 里,第三个a
就在前头不远处,是当前的时间戳。
最后一个部分打断点跟进去发现是由源码2里的函数d
生成,这个函数跟 API 签名的很像,都是序列化后做 MD5,从变量表中不难看出要拼接的几个部分,最终格式为<sid>;<token>;<timestamp>;<path>;5fbcVzmBJNUsw53#
,path
是影像的文件名。
计算出的 MD5 值同样转 HEX,作为第四部分,拼接后签名加上Basic
就是Authorization
头了。
写成 Python 代码:
def _get_auth(query: dict, image_name: str):
"""
DCM 文件的请求又有认证,用得是请求头,同样扒代码可以分析出来。
:param query URL 中的参数
:param image_name 图片名,是 8 位大写 HEX
"""
parts = query["sid"], query["token"], str(round(time.time() * 1000))
token = hashlib.md5(";".join(parts + (image_name, KEY)).encode()).hexdigest()
return "Basic " + ";".join(parts + (token,))
这次的网站使用 webpack 打包,没有任何混淆和加密措施,随便一搜就能找到认证算法,逆向难度基本为 0,适合入门练手。
其实这种爬虫本不该存在:在线报告就应该提供片子的下载,或者全国互联,患者可以把任何医院拍的片子发给另一家,但实际却是不断有人问我怎么爬这些网站,只能说互联网改造的道路还很长呐。