上一節,我們講了如何配置Charles代理,這一節我們通過模擬微博登入這個例子來看看如何使用Charles分析網站載入流程,順便把微博模擬登入的Python程式碼也給實現了。
1. 用Charles記錄整個登入過程
首先,我們執行Charles並開始記錄。然後開啟Chrome瀏覽器,選擇使用Charles代理,開啟微博首頁 ,出現登入頁面(如果之前登入過微博,要先退出登入)。輸入使用者名稱和密碼進行登入,登入成功後就可以停止Charles的記錄。這樣我們就用Charles完整記錄下了微博的登入過程。見圖:
我們把整個登入過程寫出一個Python類,它的定義為:
class WeiboLogin:
user_agent = (
'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.11 (KHTML, like Gecko) '
'Chrome/20.0.1132.57 Safari/536.11'
)
def __init__(self, username, password, cookies_tosave='weibo.cookies'):
self.weibo_user = username
self.weibo_password = password
self.cookies_tosave = cookies_tosave
self.session = requests.session()
self.session.headers['User-Agent'] = self.user_agent
接下來我們分析登入過程,並逐一實現這個類的各個方法。
2. 分析登入過程
把Charles的主視窗切換到“Sequence”標籤頁,
我們可以按載入時間順序觀察Charles記錄的微博登入過程,我們發現第一個可疑的請求的Host是:
- login.sina.com.cn
點選該條記錄,下方出現該條請求的完整內容,它的路徑是:
GET /sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack&su=&rsakt=mod&client=ssologin.js(v1.4.19)&_=1542456042531 HTTP/1.1
這個GET請求的引數_=1542456042531看起來是個時間戳,這個在ssologin.js(看後面是如何找到的)定義為preloginTimeStart,可以用int(time.time()*1000)得到。
從prelogin.php這個名字看,它是一個預登陸,即在你輸入使用者名稱和密碼前,它先從伺服器拿點東西過來:
用Python實現這個prelogin:
def prelogin(self):
preloginTimeStart = int(time.time()*1000)
url = ('https://login.sina.com.cn/sso/prelogin.php?'
'entry=weibo&callback=sinaSSOController.preloginCallBack&'
'su=&rsakt=mod&client=ssologin.js(v1.4.19)&'
'_=%s') % preloginTimeStart
resp = self.session.get(url)
pre_login_str = re.match(r'[^{]+({.+?})', resp.text).group(1)
pre_login = json.loads(pre_login_str)
pre_login['preloginTimeStart'] = preloginTimeStart
print ('pre_login 1:', pre_login)
return pre_login
這些預先拿過來的東西有什麼用呢?目前為止還不知道,繼續往下看。
補充:關於認證碼
昨天最初寫這篇教程的時候,沒有碰到驗證碼。今天就碰到驗證碼跳出來了,真是大快人心,可以把這部分補充上了。
對比昨天的prelogin的URL引數不能發現,今天的多了兩個引數:
- su=xxxxx 就是加密的那個(實為base64編碼)使用者名稱
- checkpin=1 告訴伺服器要檢查驗證碼(我去,自己寫爬蟲絕對不會這麼幹)
帶著這兩個引數請求伺服器,返回來的也會多了showpin的值:
既然要顯示pin(驗證碼),就要下載驗證碼,它的地址是:
https://login.sina.com.cn/cgi/pin.php?r=2855501&s=0&p=aliyun-a34a347956ab8e98d6eb1a99dfddd83bc708
這個是怎麼來的呢?直接按Ctrl+F 開啟“Text to Find”視窗搜尋“pin.php”:
這個Find視窗很有用,它讓我們可以在記錄的所有請求和響應裡面查詢特定文字,並且它還支援正規表示式、大小寫敏感、只找全詞。只找全詞,對查詢su這樣的短詞很有幫助,可以過濾大量包含它的詞,比如super。
這裡要特別說明一下,為什麼只選在”Response Body”裡面查詢。
因為我們是要找上面的URL是如何生成的,我們認為它是在某個js檔案的某段程式碼實現的,所以它一定是在 Response Body 裡面的,這樣也可以過濾掉很多無關資訊。
通過上面的過濾,直接就定位了相關程式碼,雙擊進去,再稍微一搜,就發現對應的程式碼了:
var pincodeUrl = "https://login.sina.com.cn/cgi/pin.php";
...
return pincodeUrl + "?r=" + Math.floor(Math.random() * 100000000) + "&s=" + size + (pcid.length > 0 ? "&p=" + pcid : "")
有了這個js,用Python來實現就易如反掌了,小猿們可以自己試試看。
有了驗證碼的URL,我們就用self.session下載它並儲存為檔案,在POST 所有login資料前,通過<code>pin = input('>>please input pin:')</code>
來獲取,加入到POST資料裡面一起POST傳送即可。
第二條可疑的請求的Host跟第一條一樣,路徑是:
POST /sso/login.php?client=ssologin.js(v1.4.19) HTTP/1.1
這是一條POST,我來看看它POST的資料,選擇這條記錄,點選“Contents”標籤,再點選“Form”標籤,可以看到它POST的資料:
這時候我們可以把這寫POST的引數和prelogin得到的聯絡起來了。
引數:su
這個看上去是“加密”的username,即使用者名稱。那它是怎麼加密的呢?瀏覽器執行的是JavaScript,所以我們猜測是通過JS加密的,那麼是哪段JS呢?看上面login.php路徑裡給了引數client=ssologin.js(v1.4.19),那我們就去ssologin.js裡面找找,選擇載入這個js檔案的請求,“Contents”標籤下面就會顯示JS程式碼,按Ctrl+F查詢username:
果然在這裡,其實就是用base64編碼了一下,算不上加密,於是我們就有了獲得su的方法:
def encrypt_user(self, username):
user = urllib.parse.quote(username)
su = base64.b64encode(user.encode())
return su
````
**引數:sp**
跟su同樣的思路,還是在ssologin.js裡面查詢password,我們發現了加密password的演算法:
![charles weibo login sp](https://www.yuanrenxue.com/wp-content/uploads/2018/12/charles-weibo-login-sp.png)
於是有了獲得sp的方法:
```python
def encrypt_passwd(self, passwd, pubkey, servertime, nonce):
key = rsa.PublicKey(int(pubkey, 16), int('10001', 16))
message = str(servertime) + '\t' + str(nonce) + '\n' + str(passwd)
passwd = rsa.encrypt(message.encode('utf-8'), key)
return binascii.b2a_hex(passwd)
引數:prelt
既然ssologin.js就是管登入的,那我們還是在這裡找prelt,Ctrl+F 查詢到
request.prelt = preloginTime;
原理prelt就是preloginTime的簡稱,那我們再搜尋preloginTime:
preloginTime = (new Date()).getTime() - preloginTimeStart - (parseInt(result.exectime, 10) || 0)
這裡的preloginTimeStart就是請求prelogin.php時的時間戳,result.exectime就是prelogin請求返回結果裡面的exectime。
哈哈哈,又找到了prelt的演算法,其實這個prelt就是從請求開始到現在的時間差,似乎也沒那麼重要,隨機一個就可以,不過還是用Python實現一下:
def get_prelt(self, pre_login):
prelt = int(time.time() * 1000) - pre_login['preloginTimeStart'] - pre_login['exectime']
return prelt
目前,我們已經獲得了登入的重要引數,接下來再看看登入請求的流程,在“Sequence”的 “Filter” 輸入login,我們可以看到過濾後的請求,其中前三個就是登入的先後順序:
其詳細流程就是:
- prelogin從伺服器獲得一些引數
- 把加密的使用者名稱密碼等引數POST給https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.19)
- 第2步返回的是html程式碼,html程式碼裡面重定向到另外的url (所以我們程式碼裡面也要實現這個重定向)
- 第3步返回的還是html程式碼,裡面通過JS先實現幾個跨域設定,最後重定向到另外一個url(我們也要實現這部分操作)
第4步返回的HTTP頭裡面重定向到另外的URL,request會跟隨這個重定向,不用我們實現。
用Python實現html程式碼裡面的JS重定向的方法就是,用正規表示式提取出JS程式碼裡面的重定向URL,然後用requests做GET請求。
完整的登入流程的程式碼就是:
def login(self):
# step-1. prelogin
pre_login = self.prelogin()
su = self.encrypt_user(self.weibo_user)
sp = self.encrypt_passwd(
self.weibo_password,
pre_login['pubkey'],
pre_login['servertime'],
pre_login['nonce']
)
prelt = self.get_prelt(pre_login)
data = {
'entry': 'weibo',
'gateway': 1,
'from': '',
'savestate': 7,
'qrcode_flag': 'false',
'userticket': 1,
'pagerefer': '',
'vsnf': 1,
'su': su,
'service': 'miniblog',
'servertime': pre_login['servertime'],
'nonce': pre_login['nonce'],
'vsnf': 1,
'pwencode': 'rsa2',
'sp': sp,
'rsakv' : pre_login['rsakv'],
'encoding': 'UTF-8',
'prelt': prelt,
'sr': "1280*800",
'url': 'http://weibo.com/ajaxlogin.php?framelogin=1&callback=parent.'
'sinaSSOController.feedBackUrlCallBack',
'returntype': 'META'
}
# step-2 login POST
login_url = 'https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.19)'
resp = self.session.post(login_url, data=data)
print(resp.headers)
print(resp.content)
print('Step-2 response:', resp.text)
# step-3 follow redirect
redirect_url = re.findall(r'location\.replace\("(.*?)"', resp.text)[0]
print('Step-3 to redirect:', redirect_url)
resp = self.session.get(redirect_url)
print('Step-3 response:', resp.text)
# step-4 process step-3's response
arrURL = re.findall(r'"arrURL":(.*?)\}', resp.text)[0]
arrURL = json.loads(arrURL)
print('CrossDomainUrl:', arrURL)
for url in arrURL:
print('set CrossDomainUrl:', url)
resp_cross = self.session.get(url)
print(resp_cross.text)
redirect_url = re.findall(r'location\.replace\(\'(.*?)\'', resp.text)[0]
print('Step-4 redirect_url:', redirect_url)
resp = self.session.get(redirect_url)
print(resp.text)
with open(self.cookies_tosave, 'wb') as f:
pickle.dump(self.session.cookies, f)
return True
程式碼中列印了很多資訊,方便我們過程整個登入過程。
要測試我們的實現就很簡單了:
if __name__ == '__main__':
weibo_user = 'your-weibo-username'
weibo_password = 'your-weibo-password'
wb = WeiboLogin(weibo_user, weibo_password)
wb.login()
修改為你的微博賬戶和密碼就可以測試起來啦。
練習題
本文我們提到了驗證碼的解決方法,但並沒有把相關程式碼加入到self.login()這個函式中,小猿們可以根據文中講到的方法自己新增實驗一下,看看是否能成功。
我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。
***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***