1. 程式人生 > >全程模擬新浪微博登錄(2015)

全程模擬新浪微博登錄(2015)

star php utf 版本 get lag spa ckey phoenix

非常久之前就了解過模擬登錄的過程。近期對python用的比較多,想來練練手,就想實現一下新浪微博登錄,首先隨便一搜,網上有大量的前輩們都做過了,我也細致看了一下。並且參考之後發現無法登錄。並且還有非常多細節沒有說得太清楚。同一時候網上最新的也是非常久之前的。對於最新的版本號也有一些修改,因此將我接近兩天時間的研究全過程記錄一下。

已有實現的簡要過程

網上已有實現能夠見http://www.douban.com/note/201767245/以及http://www.jb51.net/article/44779.htm。當然還有非常多其它樣例,都是比較早前的實現,整體來說大致過程都分為例如以下三步:

1 .預登陸

通過請求http://login.sina.com.cn/sso/prelogin.php來獲取客戶端對用戶password進行加密的參數。

主要列舉例如以下:

  • pubkey :客戶端使用RSA加密的公鑰
  • servertime:服務器時間。用來與用戶password一起擾亂加密
  • nonce:服務器隨機字符串,用來與用戶password一起擾亂加密

2 .登錄

通過http://login.sina.com.cn/sso/login.php?

client=ssologin.js使用POST對用戶名password進行處理,以及其它相關參數的處理後,得到這個請求返回的cookie和正文html內容。
主要涉及到的是用戶名先使用urlencode加密然後base64加密,password使用例如以下方式擾亂:

servertime + ‘\t‘ + nonce + ‘\n‘ + password

然後對擾亂後的字符串使用RSA加密。

3 .跳轉到ajaxlogin

將第二部中得到的正文html內容中的一段JavaScript代碼中的“location.replace()”中的地址用正則取出,然後對這個地址發起訪問就結束了。
上述三個步驟就是眼下已有的模擬登錄的簡要概述。可是眼下微博已經做了一些修改,有非常多細節須要註意。具體記錄例如以下。全部實現封裝在一個類中。具體步驟的代碼片段都在這個環境下。

prelogin請求

這個請求在用戶輸入微博名稱之後,選中用戶password輸入框時。會使用ajax方式請求一遍,獲取用戶輸入的微博登錄名實時相應的隨機信息。請求參數例如以下:

entry:weibo
callback:sinaSSOController.preloginCallBack
su:base64.encode(urlencode(username))
rsakt:mod
checkpin:1
client:ssologin.js(v1.4.18)
_:1437133246747

當中能夠看出ssologin.js的版本號已經是1.4.18了。比之前列出的版本號都更新非常多版本號了。最後一個參數是客戶端的時間,單位是毫秒。

以上通過fiddler獲取。

上述python實現例如以下:

def __mtime(self):
    ‘‘‘Return the current time by milli-second‘‘‘
    return long(‘%.0f‘ % (time.time() * 1000))
    b64username = base64.b64encode(urllib.quote(self.username))
    preReqHeader = {
        ‘Accept‘:‘text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8‘,
        ‘Accept-Encoding‘:‘gzip, deflate‘,
        ‘Accept-Language‘:‘zh-CN,zh;q=0.8‘,
        ‘Cache-Control‘:‘max-age=0‘,
        ‘Connection‘:‘keep-alive‘,
        ‘User-Agent‘:‘Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36‘,

        ‘Host‘:‘login.sina.com.cn‘,
        ‘Origin‘:‘http://weibo.com‘,
        ‘Referer‘:‘http://weibo.com/‘,
    }
    plt = self.__mtime()
    payload = {
        ‘entry‘:‘weibo‘,
        ‘callback‘:‘sinaSSOController.preloginCallBack‘,
        ‘su‘: b64username,
        ‘rsakt‘:‘mod‘,
        ‘checkpin‘:‘1‘,
        ‘client‘:‘ssologin.js(v1.4.18)‘,
        ‘_‘: plt,
    }

上述請求返回了一段JavaScript代碼:

sinaSSOController.preloginCallBack({"retcode":0,"servertime":1437133178,"pcid":"xd-b0fc6894be2638ae76e6399c104101c9d433","nonce":"SVRB9M","pubkey":"EB2A38568661887FA180BDDB5CABD5F21C7BFD59C090CB2D245A87AC253062882729293E5506350508E7F9AA3BB77F4333231490F915F6D63C55FE2F08A49B353F444AD3993CACC02DB784ABBB8E42A9B1BBFFFB38BE18D78E87A0E41B9B8F73A928EE0CCEE1F6739884B9777E4FE9E88A1BBE495927AC4A799B3181D6442443","rsakv":"1330428213","showpin":0,"exectime":9})

這裏使用正則方式提取了preloginCallBack函數中的對象,並轉換為python字典對象。完整請求封裝為一個函數:

    def preLogin(self, header, payload):
        pre = requests.get(
            self.__class__.Url[‘preLogin‘],
            headers = header,
            params  = payload
        )
        #Parse the preLogin text
        if pre.status_code != 200:
            raise base.LoginError
        text    = pre.text
        dictObj = {}
        try:
            jsonStr = re.search(r‘({[^{]+?})‘, text).group(1)
            dictObj = eval(jsonStr)
        except:
            raise base.LoginError
        return dictObj, pre.headers

另外,通過實際登錄會發現,瀏覽器中的ssologin.js文件是登錄處理的核心文件,這個文件是經過先加密後壓縮了的,我進行解壓縮之後還是能夠發現非常多處理方式。

從而找到了非常多細節的答案。
首先能夠找到prelogin的處理:

this.prelogin = function(a, b) {
            var c = location.protocol == "https:" ? ssoPreLoginUrl.replace(/^http:/, "https:") : ssoPreLoginUrl,
                d = a.username || "";
            d = sinaSSOEncoder.base64.encode(urlencode(d));
            delete a.username;
            var e = {
                entry: me.entry,
                callback: me.name + ".preloginCallBack",
                su: d,
                rsakt: "mod"
            };
            c = makeURL(c, objMerge(e, a));
            me.preloginCallBack = function(a) {
                if (a && a.retcode == 0) {
                    me.setServerTime(a.servertime);
                    me.nonce = a.nonce;
                    me.rsaPubkey = a.pubkey;
                    me.rsakv = a.rsakv;
                    pcid = a.pcid;
                    preloginTime = (new Date).getTime() - preloginTimeStart - (parseInt(a.exectime, 10) || 0)
                }
                typeof b == "function" && b(a)
            };
            preloginTimeStart = (new Date).getTime();
            excuteScript(me.scriptId, c)
        };

變量e就是構造的請求參數,preloginCallBack就是回掉函數,將返回的參數進行了處理:servertime、nonce、pubkey、rsakv都是直接賦值。供下次調用,pcid這個參數是為了進行驗證碼的操作。這裏能夠忽略。

preloginTime參數

另外一個preloginTime是一個時間間隔,能夠看出是客戶端的一個計時,同一時候還減去了服務器返回的exectime這個時間值,另外通過查閱,發現最後preloginTime這個值是下一個login請求的prelt這個參數的值,這裏我使用python也進行了計時,模擬了這個參數

      preLoginDict, preRespHeaders = self.preLogin(preReqHeader, payload)
      endPre = self.__mtime()
      prelt = endPre - plt - long(preLoginDict[‘exectime‘])

login請求

參數構造

首先是構造login請求的POST參數。這裏比較重要的就是rsa加密擾亂過的password字符串,這個部分在之前的前輩們都非常突兀地就提到了是怎樣加密的(還看到有人提問是不是新浪內部的人員)。我經過解壓縮ssologin.js這個文件,找到了這個部分的出處。


首先在一個login函數中通過loginByConfig這個函數推斷登錄方式,終於依據默認方式。使用了loginByIframe且請求方式是POST:

loginByConfig = function() {
                if (!me.feedBackUrl && loginByXMLHttpRequest(a, b, c)) return !0;
                if (me.useIframe && (me.setDomain || me.feedBackUrl)) {
                    if (me.setDomain) {
                        document.domain = me.domain;
                        !me.feedBackUrl && me.domain != "sina.com.cn" && (me.feedBackUrl = makeURL(me.appLoginURL[me.domain], {
                            domain: 1
                        }))
                    }
                    loginMethod = "post";
                    var d = loginByIframe(a, b, c);
    .....

然後在loginByIframe函數中調用了makeRequest函數,這個函數構造了這個POST請求的參數:

makeRequest = function(a, b, c) {
                var d = {
                    entry: me.getEntry(),
                    gateway: 1,
                    from: me.from,
                    savestate: c,
                    useticket: me.useTicket ? 1 : 0
                };
                me.failRedirect && (me.loginExtraQuery.frd = 1);
                d = objMerge(d, {
                    pagerefer: document.referrer || ""
                });
                d = objMerge(d, me.loginExtraFlag);
                d = objMerge(d, me.loginExtraQuery);
                d.su = sinaSSOEncoder.base64.encode(urlencode(a));
                me.service && (d.service = me.service);
                if (me.loginType & rsa && me.servertime && sinaSSOEncoder && sinaSSOEncoder.RSAKey) {
                    d.servertime = me.servertime;
                    d.nonce = me.nonce;
                    d.pwencode = "rsa2";
                    d.rsakv = me.rsakv;
                    var e = new sinaSSOEncoder.RSAKey;
                    e.setPublic(me.rsaPubkey, "10001");
                    b = e.encrypt([me.servertime, me.nonce].join("\t") + "\n" + b)
                } else if (me.loginType & wsse && me.servertime && sinaSSOEncoder && sinaSSOEncoder.hex_sha1) {
                    d.servertime = me.servertime;
                    d.nonce = me.nonce;
                    d.pwencode = "wsse";
                    b = sinaSSOEncoder.hex_sha1("" + sinaSSOEncoder.hex_sha1(sinaSSOEncoder.hex_sha1(b)) + me.servertime + me.nonce)
                }
                d.sp = b;
                try {
                    d.sr = window.screen.width + "*" + window.screen.height
                } catch (f) {}
                return d
            }

這個函數裏面就非常明顯地用if推斷了loginType。對於眼下的rsa加密方式,有例如以下代碼:

var e = new sinaSSOEncoder.RSAKey;
e.setPublic(me.rsaPubkey, "10001");
b = e.encrypt([me.servertime, me.nonce].join("\t") + "\n" + b)

另外也有之前版本號的兩次sha1加密的方式,只是眼下好像都是使用rsa方式。

b = sinaSSOEncoder.hex_sha1("" + sinaSSOEncoder.hex_sha1(sinaSSOEncoder.hex_sha1(b)) + me.servertime + me.nonce)

python實現例如以下,參數preObj是前一步prelogin請求返回的內容裏面的函數調用參數對象轉換為了python的字典對象。

    def encryptPassword(self, pw, preObj):
        import rsa, binascii
        if not isinstance(pw, types.StringType):
            return None
        n = int(preObj[‘pubkey‘], 16)  #Convert the 16 string n to number
        e = int(‘10001‘, 16) #Convert the 16 string e to number
        message = str(preObj[‘servertime‘]) + ‘\t‘ +                   str(preObj[‘nonce‘]) + ‘\n‘ + str(pw)
        key = rsa.PublicKey(n, e)
        sp  = rsa.encrypt(message, key)
        return binascii.b2a_hex(sp)

除此之外,還有prelt參數在前面提到過。能夠直接加入。

su參數是加密後的用戶名。

nonce、rsakv、servertime都直接加入。其余都固定不變就可以。

請求頭的構造

請求頭裏面須要設置Content-Type為“application/x-www-form-urlencoded”,另外還有Content-Length也須要設置,這個須要手動計算一下,特別重要的是。通過測試。發現進行login請求的Cookie中須要設置,並且這個是固定值就就能夠,直接將fiddler得到的請求的Cookie加入,都是全局值標識用戶ip和固定信息所用。

    sp = self.encryptPassword(self.password, preLoginDict)
    info(‘Get encrpyt password sucess:‘)
    info(‘password = ‘ + sp)#print sp#;exit()
    loginData = {
        ‘entry‘     : ‘weibo‘,
        ‘gateway‘   : ‘1‘,
        ‘from‘      : ‘‘,
        ‘savestate‘ : ‘0‘,
        ‘useticket‘ : ‘1‘,
        ‘pagerefer‘ : ‘http://login.sina.com.cn/sso/logout.php?entry=miniblog&r=http%3A%2F%2Fweibo.com%2Flogout.php%3Fbackurl%3D%252F‘,
        ‘vsnf‘      : ‘1‘,
        ‘su‘        : b64username,
        ‘service‘   : ‘miniblog‘,
        ‘servertime‘: preLoginDict[‘servertime‘] + (self.__mtime() - endPre),
        ‘nonce‘     : preLoginDict[‘nonce‘],
        ‘pwencode‘  : ‘rsa2‘,
        ‘rsakv‘     : preLoginDict[‘rsakv‘],
        ‘sp‘        : sp,
        ‘sr‘        : ‘1600*900‘,
        ‘encoding‘  : ‘UTF-8‘,
        ‘prelt‘     : prelt,
        ‘url‘       : ‘http://weibo.com/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack‘,
        ‘returntype‘: ‘META‘,
    }
    loginReqHeaders = preReqHeader.copy()
    conLen = len(urllib.urlencode(loginData))
    loginReqHeaders[‘Content-Length‘] = conLen
    loginReqHeaders[‘Content-Type‘] = ‘application/x-www-form-urlencoded‘
    loginReqHeaders[‘Cookie‘] = ‘固定cookie值‘
    login = requests.post(
        self.__class__.Url[‘login‘],
        headers = loginReqHeaders,
        data = loginData,
    )

這樣請求之後得到的返回信息頭部要將Cookie保存下來,並且與發送的請求Cookie合並到一起。下次請求時須要。


另外,得到的html內容例如以下:

<html>
<head>
<title>新浪通行證</title>
<meta http-equiv="Content-Type" content="text/html; charset=GBK" />

<script charset="utf-8" src="http://i.sso.sina.com.cn/js/ssologin.js"></script>
</head>
<body>
正在登錄 ...
<script>
try{sinaSSOController.setCrossDomainUrlList({"retcode":0,"arrURL":["http:\/\/crosdom.weicaifu.com\/sso\/crosdom?action=login","http:\/\/passport.97973.com\/sso\/crossdomain?action=login","http:\/\/passport.weibo.cn\/sso\/crossdomain?action=login"]});}catch(e){}try{sinaSSOController.crossDomainAction(‘login‘,function(){location.replace(‘http://passport.weibo.com/wbsso/login?

url=http%3A%2F%2Fweibo.com%2Fajaxlogin.php%3Fframelogin%3D1%26callback%3Dparent.sinaSSOController.feedBackUrlCallBack%26sudaref%3Dweibo.com&ticket=ST-NTE1MTU5MzUwMA==-1437130678-xd-9EB20B3EB2CB305249A978593E222D95&retcode=0‘);});}catch(e){} </script> </body> </html>

passport登錄請求

從上面得到的html內容能夠看到。一段JavaScript代碼中,使用的location.replace調用的是passport下的wbsso/login文件,和曾經實現的版本號並不一樣,並非直接進行ajaxlogin請求。
因此使用正則獲取到這個跳轉地址,發起請求。

login_sid_t的獲取

這裏比較重要的是,發現fiddler獲取的實際請求中cookie包括了一個”login_ sid_t”項,此時必須要加上才行,因此去回溯全部請求。發現。這個參數是在第一次請求weibo.com時生成的,並且每次都不一樣。因此又須要獲取一次。

python實現例如以下:

        weiboHeaders = {
            ‘Host‘ : ‘weibo.com‘,
            ‘User-Agent‘ : ‘Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36‘,
            ‘Cookie‘:‘TC-Ugrow-G0=0149286e34b004ccf8a0b99657f15013; SUB=_2AkMi9DzDdcNhrAFXmvEXyWjia4xRnk2l5Z-gbhmfSH1UXH4SjVcLhkcF2RF-Xtyj2Ea64VJJRk99qu8X-IjCokugCM12sNUAQM9eIag.; SUBP=0033WrSXqPxfM72wWs9jqgMF55529P9D9WFAlHfAQ6dPVKycqD2L8_sC5JpV8Jxfqgp4qg4rMcvV9XWrdg8DdF4odcXt‘,
        }
        try:
            weibo =requests.get(self.__class__.Url[‘weibo‘], headers = weiboHeaders)
        except: pass
        loginSidT = ‘‘
        if weibo.status_code == 200:
            info(‘Get "login_sid_t" success:‘)
            loginSidT = weibo.headers[‘set-cookie‘]
            loginSidT = loginSidT[0: loginSidT.find(‘;‘)]

這裏請求頭的Cookie內容也是直接套用了fiddler中的值,都是固定值直接寫死沒有影響。

請求頭構建

這裏有一個非常重要的地方就是,一定要設置好Host和Referer兩個請求頭,否則會返回無權限。我就是在這裏設置錯了耽誤了非常久的時間。
另外須要構建的就是Cookie,這裏非常重要的就是那個login_sid_t的設置。以及myuid和un的設置。

其余的參數直接使用前面的頭部信息就能夠。myuid是一個固定值。直接寫死。

可是un是當前用戶名。uid確是微博用戶的id,這些信息保存在login請求中的cookie中的SUP中,SUP使用了urlencode之後保存。須要從中解密出uid和name信息:

    def __getInfo(self, ck):
        ck = urllib.unquote(ck)
        sup = re.search(r‘SUP=([^;]+);‘, ck).group(1)
        suplist = sup.split(‘&‘)
        suplist = filter(lambda x: x.startswith(‘uid‘) or x.startswith(‘name‘), suplist)
        sup = {s[0:s.find(‘=‘)] : urllib.unquote(s[s.find(‘=‘)+1:]) for s in suplist}
        return sup

url獲取並發起請求

請求的url須要從location.replace裏面提取,使用正則提取,然後就加上之前構造的請求頭,直接發送請求就可以:

url = re.search(‘location.replace\(\‘([^\)]+)\‘\)‘, login.content)
url = url.group(1)
info(‘Get ajax url: ‘ + url)
con = requests.get(url, headers = ajaxReqHeaders)

此時,返回的內容是一段html:

<html><head><script language=‘javascript‘>parent.sinaSSOController.feedBackUrlCallBack({"result":true,"userinfo":{"uniqueid":"5151593500","userid":null,"displayname":null,"userdomain":"?wvr=5&lf=reg"},"redirect":"http:\/\/d.weibo.com\/?

from=signin"});</script></head><body></body></html>

能夠看到,回掉函數中的參數result項是true。代表登錄成功。

終於登錄

上述返回代碼在瀏覽器端是直接跳轉到給定的redirect地址就可以。可是通過分析fiddler,發現redirect地址請求之後又進行了跳轉,終於跳轉了”weibo.com/u/uid/home?

userdomain“,當中uid是前面從Cookie獲取的uid。userdomain就是這裏返回的參數中的userinfo中userdomain。
因此再次使用正則提取上述參數,並構造出終於的請求地址:

    ajaxFeedBack = re.search(r‘feedBackUrlCallBack\(([^\)]+)\)‘, con.content)
    ajaxFeedBack = ajaxFeedBack.group(1)
    ajaxFeedBack = ajaxFeedBack.replace(‘true‘, ‘True‘).replace(‘null‘, ‘None‘)
    ajaxFeedBack = eval(ajaxFeedBack)
    if not ajaxFeedBack[‘result‘]:
        info(‘Ajax login failed!‘)
        sys.exit(2)
    userdomain = ajaxFeedBack[‘userinfo‘][‘userdomain‘]
    info(‘Get userdomain sucess:‘ + userdomain)

至於請求頭。就將前面全部請求頭中的Cookie進行合並。同一時候刪除不用的請求頭,改好Host、Referer等信息,發起終於請求就可以獲取到登錄後的微博首頁內容。

    info(‘Try to get main content...‘)
    mainReqHeaders = ajaxReqHeaders.copy()
    mainReqHeaders[‘Host‘] = ‘weibo.com‘
    mainReqHeaders[‘Referer‘] = ‘http://weibo.com/‘
    mainUrl = self.__class__.Url[‘weibo‘] +               ‘/u/‘ + userinfo[‘uid‘] + ‘/home‘ + userdomain
    main = requests.get(mainUrl, headers = mainReqHeaders)
    if main.status_code == 200:
        info(‘Login success.‘)
        info(main.content)
        return mainReqHeaders
    info(‘Login failed!‘)

至此,模擬登錄就算實現了,並且是返回了微博登錄後的首頁內容。有一個要說明的地方是,盡管能夠使用python標準庫中管理cookie的cookielib等模塊,但我這裏還是當做字符串進行處理的,我的考慮是能夠按自己須要每次請求僅僅構造必需的Cookie。當然這個前提是須要多次進行試驗,另外一個方面是能夠鍛煉一下考慮是否全面的思維,特別是處理Cookie提取中expires的問題等等。
歡迎交流和指正~_~

全程模擬新浪微博登錄(2015)