一,背景
業(yè)務(wù)背景
模擬賬號登陸,抓取 繳費周期 本期繳納 繳費基數(shù) 等信息。這里可能會有國杠1573會杠:"為什么 自己知道賬號還抓取這些數(shù)據(jù)有什么意義"。類似這種問題此處不做解釋。這里只談?wù)撟ト〉乃悸泛头椒ǎ唧w哪家社保網(wǎng)站此處為了避嫌就不指名道姓了,以免給維護(hù)這家社保網(wǎng)站的同行造成困擾。希望大家就算知道是哪家網(wǎng)站也不要公布,大家都混口飯吃,都不容易??瓤?~~ 閑扯了。
技術(shù)背景贅述
由于該網(wǎng)站使用的是flex技術(shù)開發(fā)的web端 ,所以這里講的方法理論支持所有flex網(wǎng)站的爬去。引用下擺渡的flex介紹:
Flex 是一個高效、免費的開源框架,可用于構(gòu)建具有表現(xiàn)力的 Web應(yīng)用程序,這些應(yīng)用程序利用Adobe Flash Player和Adobe AIR, 可以實現(xiàn)跨瀏覽器、桌面和操作系統(tǒng)。雖然只能使用 Flex 框架構(gòu)建 Flex應(yīng)用程序,但Adobe Flash Builder(之前稱為 Adobe Flex Builder)軟件可以通過智能編碼、交互式遍歷調(diào)試以及可視設(shè)計用戶界面布局等功能加快開發(fā)。
使用 Flex 創(chuàng)建的 RIA 可運行于裝有 Adobe Flash Player 插件的瀏覽器中,或運行于跨操作系統(tǒng)的 Adobe AIR上,它們可以跨所有主流瀏覽器、操作系統(tǒng)實現(xiàn)一致的運行。通過利用 AdobeAIR,F(xiàn)lex應(yīng)用程序可以訪問本地數(shù)據(jù)和系統(tǒng)資源。Flex技術(shù)的三大組成部分:UI、數(shù)據(jù)、服務(wù)器技術(shù)介紹。從根本上說,F(xiàn)lex技術(shù)是表現(xiàn)層解決方案,像所有其他類似技術(shù)一樣,表現(xiàn)層技術(shù)要解決三個基本問題:表現(xiàn)層界面展示和人機交互,客戶端數(shù)據(jù)操作及服務(wù)器端數(shù)據(jù)交互和整合。Flex針對這三個根本問題提供了卓越的解決方案。
使用 Flex 創(chuàng)建的 RIA 可運行于裝有 Adobe Flash Player 插件的瀏覽器中,或運行于跨操作系統(tǒng)的 Adobe AIR上,它們可以跨所有主流瀏覽器、操作系統(tǒng)實現(xiàn)一致的運行。通過利用 AdobeAIR,F(xiàn)lex應(yīng)用程序可以訪問本地數(shù)據(jù)和系統(tǒng)資源。Flex技術(shù)的三大組成部分:UI、數(shù)據(jù)、服務(wù)器技術(shù)介紹。從根本上說,F(xiàn)lex技術(shù)是表現(xiàn)層解決方案,像所有其他類似技術(shù)一樣,表現(xiàn)層技術(shù)要解決三個基本問題:表現(xiàn)層界面展示和人機交互,客戶端數(shù)據(jù)操作及服務(wù)器端數(shù)據(jù)交互和整合。Flex針對這三個根本問題提供了卓越的解決方案。
flex通訊協(xié)議使用的是 AMF 協(xié)議,所以我們下面的python3.7技術(shù)棧中會使用 amf 相關(guān)模塊。
二,技術(shù)棧
語言:python3.7.4
ide工具:pyCharm
使用的模塊:PyAMF2 (0.6.1.5)| pycryptodome (做AES加密用)
-----------2021年4月13日 更新----------------------------
這里安裝 pyamf2 模塊已經(jīng)403、該模塊已經(jīng)改為 py3amf
直接:pip3 install py3amf 即可
------------------2021年4月13日 更新---------------------
分析工具:Charles (抓包)這里不講這個工具的使用,需自行擺渡 。swf文件反編譯 解壓密碼:python3
還有常用的模塊這里不 一 一 說明安裝了 看如下截圖:
如果還有使用python2的同學(xué)這里可以使用這個模塊 PyAMF (0.8.0),AES加密的自行網(wǎng)上查找對應(yīng)py2的模塊

三,分析
first of all
原理
一個http請求 會有一個響應(yīng) 我們要做的是 接收基于amf協(xié)議響應(yīng)數(shù)據(jù)的處理和模擬amf協(xié)議的請求,由于傳輸?shù)氖嵌M(jìn)制流,這里不管他的使用的是什么方式傳輸,對于python的熱火程度肯定有相關(guān)的模塊來處理,這里使用了pyamf 模塊幫我們封裝流的操作,現(xiàn)在只需要關(guān)注要做的業(yè)務(wù)就好。我們只需要根據(jù)pyamf 模塊的請求和解析方式來封裝數(shù)據(jù)結(jié)構(gòu)然后構(gòu)造請求就行。當(dāng)然模擬請求 添加header 頭是少不了的 。后面我會 放出源碼,展示這一個完整的過程。
secondly
1,打開要爬去的目標(biāo)網(wǎng)站 F12打開調(diào)試模式 點擊網(wǎng)絡(luò) 注意觀察請求次數(shù)和路徑,打開 charles 軟件 一般是打開默認(rèn)就開啟抓包。

2, 開始輸入賬號密碼登陸 直到登陸完成 按下 charles軟件中的stop record 按鈕
從上圖可以看到這么幾個amf請求,每一個amf 就可以理解為一次amf數(shù)據(jù)交互 ,

3,點擊 onlineServiceActionPersonNormal.do 請求我們可以看到提交的表單數(shù)據(jù)這里有三個參數(shù),可以猜測是賬號,密碼還有類型。注意這個請求響應(yīng)碼是302 說明重定向了。這里可以確定這一步是登陸,到這里我們要思考登陸了之后swf文件怎么知道我們當(dāng)前用戶已經(jīng)登陸?這里的重定向到哪里去了?帶著這兩個問題到下一步
4,看到sessionId的東西直覺感覺有用先記一下,在瀏覽器請求的時候
一圖介紹charles如何看

分析結(jié)果:
上圖介紹了如何找到swf文件以及下載,下載完成后就可以使用我們文章中提到的 反編譯工具去查看源碼,從源碼分析。我們知道加密方式為AES加密

在打開的目標(biāo)網(wǎng)頁的瀏覽器中
F12 或者 mac版的chrome cmd + option+ I 打開瀏覽器調(diào)試。控制臺中 輸入此函數(shù)得到如下結(jié)果,從擺渡找一個在線aes加密的網(wǎng)頁驗證下加密方式和結(jié)果,從而得到加密的方式和類型如下截圖:

上圖16位加密結(jié)果一致有可能是 不足16位補全方法不一樣造成的
<u>
上js運行結(jié)果和在線AES加密算法的值作比較。發(fā)現(xiàn)加密結(jié)果是一致的 說明目標(biāo)網(wǎng)站加密算法和在線加密算法一致,那我們只需要找到密文去進(jìn)行解密就可以拿到未加密的字符串。

經(jīng)過一番查看反編譯源碼的努力我找到了他是加載其中的響應(yīng)參數(shù)key來作為加密的密碼。當(dāng)然 反編譯看源碼的功勞少不了
<strong>。通過這一步可以通過在線AES解碼來獲得未加密的字符串。拿到這串?dāng)?shù)字我們需要猜想 這個可能是 什么根據(jù)直覺我這里獲取到的是個七位的數(shù)字。我猜想可能是個人信息的編碼之類的。后來在基礎(chǔ)信息模塊點擊查詢 確實看到了這個七位數(shù)字。那么到這里就可以完全的寫出來查詢程序了。
知道這么些信息基本差不多可以模擬請求查詢數(shù)據(jù)了
代碼區(qū)
python3小白一枚,代碼難看了點,期待python高手優(yōu)化,重點參照思路.
代碼環(huán)境文章開頭有說 這里啰嗦一下 python3.7
寫篇原創(chuàng)實屬不易,如果對你有啟發(fā),我就心滿意足了??梢缘脑捴x謝各位朋友點個贊
import urllib
import http.cookiejar
from urllib import request
import uuid
import pyamf
import json, datetime
from pyamf import remoting
from pyamf.flex import messaging
import operator as op
from Crypto.Cipher import AES
from binascii import b2a_hex, a2b_hex
from Crypto import Random
import base64
class httpBuilder:
def __init__(self):
self.header = None
self.url = None
self.postData = None
targetUrl = "http://www.xxxx.com/messagebroker/amf";
httpBuilder.url = "http://www.xxxx.com/onlineServiceActionPersonNormal.do"
httpBuilder.header = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "utf-8",
"Accept-Language": "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",
"Connection": "keep-alive",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0",
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": 94,
}
httpBuilder.postData = urllib.parse.urlencode({
"normalPersonUserName": "username",
"normalPersonPassWord": "password",
"normalPersonUserType": "0"
}).encode('utf-8')
req = urllib.request.Request(httpBuilder.url, httpBuilder.postData, httpBuilder.header)
# 自動記住cookie
cookie = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie))
r = opener.open(req)
print('重定向完畢獲取 session :%s ' % r.url)
##截取獲取 sessionId
sessionId = r.url[r.url.find('=') + 1:]
print('截取后字符串 sessionId : %s ' % sessionId)
#######html登陸結(jié)束
### AES 加密
def aes_encrypt(data, password):
bs = AES.block_size
pad = lambda s: s + (bs - len(s) % bs) * chr(bs - len(s) % bs)
cipher = AES.new(password.encode('utf-8'), AES.MODE_ECB)
data = cipher.encrypt(pad(data).encode('utf-8'))
return base64.b64encode(data)
# 這里需要查詢到請求加密的 編號 以便用來生成查詢的token
class userInfo:
def __init__(self):
self = {}
pyamf.register_class(userInfo, alias='flex.messaging.messages.CommandMessage')
getLoginUserSessionMsg = messaging.RemotingMessage(messageId=str(uuid.uuid1()).upper(),
clientId=None,
operation='getLoginUserSession',
destination='appLogin',
timeToLive=0,
timestamp=0)
class grzhInfo:
def __init__(self):
self = {}
pyamf.register_class(grzhInfo, alias='flex.messaging.messages.CommandMessage')
userNoMsg = messaging.RemotingMessage(messageId=str(uuid.uuid1()).upper(),
clientId=None,
operation='queryGrzhList',
destination='infoPersonManager',
timeToLive=0,
timestamp=0)
class decodeInfo:
def __init__(self):
self = {}
pyamf.register_class(decodeInfo, alias='flex.messaging.messages.CommandMessage')
decodeMsg = messaging.RemotingMessage(messageId=str(uuid.uuid1()).upper(),
clientId=None,
operation='queryAa10_table',
destination='aa10',
timeToLive=0,
timestamp=0)
## 查詢應(yīng)繳實繳
class queryXY:
def __init__(self):
self = {}
# 這里構(gòu)造要查詢的方法
pyamf.register_class(queryXY, alias='flex.messaging.messages.CommandMessage')
yjsjModelMsg = messaging.RemotingMessage(messageId=str(uuid.uuid1()).upper(),
clientId=None,
operation='queryAc20ByAac001',
destination='infoPersonManager',
timeToLive=0,
timestamp=0)
def getRequestData(msg, MsgBody, reqType):
msg.body = MsgBody
msg.headers['DSEndpoint'] = 'my-amf'
msg.headers['DSId'] = str(uuid.uuid1()).upper()
# 按AMF協(xié)議編碼數(shù)據(jù)
req = remoting.Request('null', body=(msg,))
env = remoting.Envelope(amfVersion=pyamf.AMF3)
env.bodies = [(reqType, req)]
data = bytes(remoting.encode(env).read())
return data
# 獲取響應(yīng)源數(shù)據(jù)
def getResponse(data):
req = urllib.request.Request(targetUrl, data, headers={'Content-Type': 'application/x-amf'})
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie))
return opener.open(req).read()
def getReslutList(response):
amf_parse_info = remoting.decode(response)
list = amf_parse_info.bodies[0][1].body.body['resultList']
return list
def getKey(response):
amf_parse_info = remoting.decode(response)
keyList = amf_parse_info.bodies[0][1].body.body['key']
for k in keyList.keys():
return k;
def getUserNo(response):
amf_parse_info = remoting.decode(response)
list = amf_parse_info.bodies[0][1].body.body['resultList']
for item in list:
return item['aac001']
# 解析查詢的數(shù)據(jù)值
def builderResult(response):
amf_parse_info = remoting.decode(response)
list = amf_parse_info.bodies[0][1].body.body['resultList']
return list
# 獲取身份證身份證號
reqData = getRequestData(getLoginUserSessionMsg, [sessionId], '/2');
responseData = getResponse(reqData)
resultList = getReslutList(responseData);
userId = None
for a in resultList:
userId = a['aac002']
# 獲取加密密鑰
decodeReqData = getRequestData(decodeMsg, [], '/3');
decodeResponseData = getResponse(decodeReqData)
useKey = getKey(decodeResponseData);
print(useKey)
# 獲取個人編號
# 獲取 個人編號 aes加密 后的值
userIdenCode = aes_encrypt(userId, useKey)
#
userNoReqData = getRequestData(userNoMsg, [userIdenCode], '/3');
# 獲得響應(yīng)結(jié)果的身份證號
userNoResponse = getResponse(userNoReqData)
userNo = getUserNo(userNoResponse);
queryToken = aes_encrypt(userNo, useKey)
queryToken = queryToken.decode('utf-8')
print(queryToken)
# 這里才開始請求真正的數(shù)據(jù) queryToken最重要 前面幾個步驟就是為了拿它 后面這些數(shù)字就是常規(guī)的查詢菜蔬
yjsjQueryParam = [queryToken,
"'11','12','14','15','21','31','32','33','35','36','41','51','61','1','2','3','4','5','302','301'",
"'10','20'", "'0','1'", "199001", "209912", False]
yjsjModelReqData = getRequestData(yjsjModelMsg, yjsjQueryParam, '/2');
yjsjModelResponse = getResponse(yjsjModelReqData)
yjsjResultList=builderResult(yjsjModelResponse);
for item in yjsjResultList:
print(' 費款所屬期 %s , 本期繳納 %s , 繳費基數(shù) %s ' % (str(item['aae003']),str(item['aac123']),str(item['aac150'])))