1、開局閑聊
昨天下午准備隨便找找高鐵票時,突然對12306的登錄產生了興趣,於是研究了一下,先說明兩點:
- 驗證碼
這部分調用的是一個大佬現成的API,如果各位大佬對識別驗證碼部分感興趣,我這個小菜雞很抱歉幫不上忙 - 登錄表單
實際上12306登錄的表單十分簡單,字段少JS代碼基本沒什么難度,所以想看高階JS逆向的姐姐可能要失望了
既然沒什么難點,那有人可能就問了為什么還要水一篇文章?
原因就是12306登錄過程中還是有不少坑,雖然單個頁面請求字段少,但頂不住一個登錄請求居然有十來個請求頁面,在這個過程中各種自動手動設置Cookie,如果你憨憨的只請求登錄url是永遠失敗的(別問我怎么知道的!),浪費我好幾個小時時間。
2、准備工具
- 瀏覽器開發者工具
- Fiddler或Charles等抓包工具
建議使用抓包工具,因為請求太多時控制台不是很直觀,控制台主要用於JS調試,雖然登錄12306基本不需要用到調試
3、請求分析
首先運行抓包工具監聽,在瀏覽器打開12306的主頁手動登錄一次,登錄成功后就可以抓包監聽了,我這里用的是Charles
顯示的結果有很多,但是不要怕,大部分都是靜態文件,除掉靜態文件其他相關請求大概也就十五六個...
我一開始被N次登錄失敗搞上頭了總覺得12306在請求上埋坑於是還真按着這十多個請求一路寫下去...
后來想想其實沒必要這么做,關注主要的幾個登錄請求中的Cookie字段來源再針對性發出請求就好了,事后也證明只需要經過6個請求,就能成功登錄。
所以本文我會着手從關鍵的幾個登錄請求分析,就不贅述摸索過程的痛了。
3.1 獲取驗證碼
生成驗證碼的URL很容易找到
請求字段
請求字段基本上都是固定值,callback和_還有1589797925252是和時間戳相關的,但服務器壓根不驗證callback和_,所以我們不用管
Cookie和響應
響應image就是經過base64驗證碼圖片,驗證碼圖片從這里就可以拿到了
重點說Cookie,雖然字段有點多,但其實只要關注使用JS手動設置的字段就可以了,因為Session會幫我們處理響應頭里的Cookie,重點就是確定哪些字段不是通過響應頭設置的,可以直接搜索字段,比如第一個_passport_session
結果顯示在某一個請求的響應頭中存在Set-Cookie: _passport_session 那我們就可以暫時放下不管
再比如RAIL_EXPIRATION字段
在一個JS文件中出現了該字段,這表明是手動添加的,需要重點關注,既然是手動添加的字段要么是JS計算生成要么是服務器響應,可以先找找字段值
成功在響應中找到了值,點進去看看
首先分析響應有三個字段exp、cookieCode和dfp,經比對exp和dfp就是驗證碼請求Cookie中的RAIL_EXPIRATION和RAIL_DEVICEID
其次看看這個請求本身:
請求參數雖然有很多,但瀏覽器打開是可以直接響應結果的,所以暫且不管它,實際上這些參數的生成過程還是有點意思的,過兩天我會專門寫一篇分析這部分JS參數的文章,現在我們直接使用訪問拿到的結果就能通過
回到獲取驗證碼的請求cookie上來,經過分析發現除開RAIL_EXPIRATION和RAIL_DEVICEID其他字段都是響應頭設置的,所以在獲取驗證碼圖片這一步我們只需要進行兩個請求:
import re
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36',
'Referer': 'https://kyfw.12306.cn/otn/resources/login.html',
'Host': 'kyfw.12306.cn'
}
session = requests.Session()
# p1這些字段其實基本就是獲取設備信息再進行加密處理
p1 = {
'algID': 'y6fvmhGlLP',
'hashCode': 'WRXET1wCtYsDWujgBBDiq2A4aqJOy-G6t5VK5OI0wNY',
'FMQw': 0,
'q4f3': 'zh-CN',
'VPIf': 1,
'custID': 133,
'VEek': 'unknown',
'dzuS': 0,
'yD16': 0,
'EOQP': 'c227b88b01f5c513710d4b9f16a5ce52',
'jp76': '52d67b2a5aa5e031084733d5006cc664',
'hAqN': 'MacIntel',
'platform': 'WEB',
'ks0Q': 'd22ca0b81584fbea62237b14bd04c866',
'TeRS': '709x1280',
'tOHY': '24xx800x1280',
'Fvje': 'i1l1o1s1',
'q5aJ': '-8',
'wNLf': '99115dfb07133750ba677d055874de87',
'0aew': headers['User-Agent'],
'E3gR': '92271eade53193a7130e280652b8e939',
'timestamp': int(time.time() * 1000)
}
# 第一次請求獲取exp和dfp
r1 = session.get('https://kyfw.12306.cn/otn/HttpZF/logdevice', params=p1, headers=headers)
exp = re.search(r'exp":"(\d+)",', r1.text).group(1)
dfp = re.search(r'dfp":"(.+?)"', r1.text).group(1)
cookieCode = re.search(r'cookieCode":"(.+?)"', r1.text).group(1) # 這個字段暫時不確定作用
session.cookies.update({'RAIL_DEVICEID': dfp, 'RAIL_EXPIRATION': exp}) # 手動添加cookie
# 第二次請求獲取驗證碼
p2 = {
'login_site': 'E',
'module': 'login',
'rand': 'sjrand',
str(int(time.time() * 1000)): ''
}
r2 = session.get('https://kyfw.12306.cn/passport/captcha/captcha-image64', params=p2)
image = re.search(r'image":"(.+?)",', r2.text).group(1)
驗證碼圖片就成功拿到了
3.2 提交驗證結果
如上圖,在點擊登錄后的第一步是發起一個ajax請求判斷驗證碼是否正確
老規矩,上URL
先分析請求參數,發現除了answer也就是驗證碼答案是動態的,其他都是靜態的,那這個答案197,125,26,129是怎么整出來的呢,這時候用瀏覽器分析頁面結構:
先隨便在驗證碼圖片上點一下,然后右鍵檢查元素
發現當在圖片上標記一個點,頁面就會生成一個div標簽,很簡單就能推測出randcode就是提交答案,它的數值就是圖片所在的平面位置點,我們可以隨意的在8個圖中取一個點作為第幾張圖的映射,如
position = {
1: '49,48', 2: '124,52', 3: '200,43', 4: '259,47',
5: '50,113', 6: '101,102', 7: '198,112', 8: '250,127'
}
def getVerifyResult(path: str):
"""調用API接口獲取驗證碼結果
:param path: 驗證碼圖片路徑
:return:
"""
url = "http://littlebigluo.qicp.net:47720/"
ret = []
# 發送post請求把圖片數據帶上
file = open(path, 'rb')
response = requests.post(url, data={"type": "1",},
files={'pic_xxfile': file})
file.close()
# 返回識別結果
for i in re.findall("<B>(.*)</B>", response.text)[0].split(" "):
ret.append(position[int(i)])
return ret
接下來調用上面的識別函數獲取答案
import base64
code_path = "code.jpg"
imgdata = base64.b64decode(image)
with open(code_path, 'wb') as f:
f.write(imgdata)
capchat = getVerifyResult(code_path)
answer = ','.join(capchat)
Cookie和響應,都是老東西中規中矩沒有需要注意的
在提交驗證結果這一步,只需要一個請求
p3 = {
'answer': answer,
'rand': 'sjrand',
'login_site': 'E'
}
# 第三次請求 提交驗證碼答案
r3 = session.get('https://kyfw.12306.cn/passport/captcha/captcha-check', headers=headers, params=p3)
j3 = r3.json()
if j3.get('result_message') != '驗證碼校驗成功':
raise RuntimeError('【驗證碼異常】: ' + j3.get('result_message'))
3.3 提交登錄表單
URL:
表單字段就4個,賬號密碼都是明文,answer是上一步的驗證碼答案
響應結果是一段json,其中返回了一段比較長的字段uamtk,先記下來暫且不管
r4 = session.post('https://kyfw.12306.cn/passport/web/login',
data={'username': user,
'password': password,
'appid': 'on',
'answer': answer},
headers=headers)
j4 = r4.json()
if j4.get('result_message') != '登錄成功':
raise RuntimeError('【登錄失敗】: ' + j4.get('result_message'))
uamtk = j4['uamtk']
雖然返回登錄成功,但你可千萬別天真的相信了,這個cookie依然沒通過認證
有同學可能注意到login后面緊貼的2個請求,但經驗證這兩個請求可有可無,從它們不涉及生產任何cookie也可以驗證這一點
3.4 登錄驗證
繼續尋找請求記錄,發現了一個顯眼的URL請求
注意到Cookie里面出現的新字段uamtk就是login返回的uamtk,可以直接安排上
響應也有一串字符串newapptk,老規矩記下來再說
接下來看看提交的表單
喲,又是白給,可以很輕松的構建這個請求
r5 = session.post('https://kyfw.12306.cn/passport/web/auth/uamtk',
headers={'User-Agent': headers['User-Agent'],
'Referer': 'https://kyfw.12306.cn/otn/passport?redirect=/otn/login/userLogin',
'Origin': 'https://kyfw.12306.cn',
'Host': 'kyfw.12306.cn'},
data={'appid': 'otn'})
j5 = r5.json()
if j5.get('result_message') != '驗證通過':
raise RuntimeError('【驗證失敗】: ', j5.get('result_message'))
tk = j5['newapptk']
此時此刻依然還未通過驗證,繼續找下去
url:
為什么會找到它呢因為響應很顯眼啊,都返回用戶名了重點關注准沒錯
可以看到表單一個字段tk,不就是上一個請求給我們的newapptk嘛
再看Cookie
沒有新鮮玩意,直接構建請求就完事了
r6 = session.post('https://kyfw.12306.cn/otn/uamauthclient',
data={'tk': tk},
headers=headers)
j6 = r6.json()
if j6.get('result_message') != '驗證通過':
raise RuntimeError('【驗證失敗】: ', j6.get('result_message'))
這就是最后一個請求了,沒有更多了...
3.5 驗證登錄是否成功
經過前面的6個請求,我們的session已經可以使用了,由於12306的信息基本用ajax獲取,所以訪問普通頁面是得不到信息的,這里可以使用一個api來判斷是否登錄成功:
url = 'https://kyfw.12306.cn/otn/index/initMy12306Api'
r = session.post(url)
if '"status":true' in r.text:
print('登錄成功:', r.json()['data']['user_name'])
else:
raise RuntimeError('登錄失敗...')
至此,模擬登錄就完成了,cookie可以保留到本地,下次登錄直接使用cookie就不用這么折騰了。
4、完整代碼
我把整個模擬登錄代碼封裝好了,目前只實現了模擬登錄並上傳到了Github,有興趣的小伙伴可以去看看,如果對你有幫助歡迎點個Star