[Python] 微博模拟登陆研究

NOTES:

  • 这篇博文迁移自旧博客,图片已经丢失了,但应该不影响阅读
  • 写于四年前,现在的微博登陆接口可能发生了变化,我不保证这些代码还能用。

写博客的时候我还在读大一,对编程所知甚少,其用到的工具显然有更好的选择,代码也肯定存在不少缺陷。

正文

在我的不懈努力(瞎折腾)下,终于自力更生地搞定了这个登陆验证的难题。。在这里写一下这个艰难的过程

环境

tools:

  1. chrome及其 developer tools(为了避免cookies影响使用隐身模式)
  2. idle
  3. pycharm

因为微博登陆有好几个跳转,所以开启developer tools 的preserve log模式。 另外,为了快速格式化js代码,使用了webstorm 工具。(其实我是懒得弄vim的相应插件=w=)

python3.5 libs

Lib Note
urllib2 在python3中应使用 urllib.request
cookielib python3中变成了 http.cookiejar
json 用于对拉取的数据进行parsing , 以方便处理
binascii 用于对加密数据进行编码
rsa
base64
re

其中urllib2, cookielib 和rsa 是第三方库,需要pip install xxx

据说移动端的微博爬起来简单。。但是没挑战的事儿才不要干呢!对吧。 目标是用python实现模拟登陆的功能,传入用户名和密码,返回登录cookies。

访问一下 weibo.com

当所有网页元素加载完后,发现每隔一两秒就出现一个奇怪的request

其中对应的js代码是长这样子:


window.STK_1458563180499289 && STK_1458563180499289({"retcode": 50114001, "msg": "\u672a\u4f7f\u7528", "data": null});

当输入用户名(不提交)时,出现了一个prelogin请求,同时刚才的轮询停止

看一下prelogin 的url:

http://login.sina.com.cn/sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack&su=Mjk0ODI5MTQ3JTQwcXEuY29t&rsakt=mod&checkpin=1&client=ssologin.js(v1.4.18)&_=1458639835300

这里面有两个比较可疑的地方,一是su=XXXXXXX 二是最后的一串数字。 用base64 解码一下su试试:

import base64
print(base64.b64decode('Mjk0ODI5MTQ3JTQwcXEuY29t'))

输出结果是: b'294829147%40qq.com' 嗯,猜对了,su就是用户名,而且是url编码再base64编码。

然后再看prelogin中response的body部分:

sinaSSOController.preloginCallBack({
    "retcode": 0,
    "servertime": 1458559375,
    "pcid": "gz-6619a7a236d3f111fceb12be3f9cecfaa20a",
    "nonce": "HV1TC8",
    "pubkey": "EB2A38568661887FA180BDDB5CABD5F21C7BFD59C090CB2D245A87AC253062882729293E5506350508E7F9AA3BB77F4333231490F915F6D63C55FE2F08A49B353F444AD3993CACC02DB784ABBB8E42A9B1BBFFFB38BE18D78E87A0E41B9B8F73A928EE0CCEE1F6739884B9777E4FE9E88A1BBE495927AC4A799B3181D6442443",
    "rsakv": "1330428213",
    "is_openlock": 0,
    "showpin": 0,
    "exectime": 12
})

这里有几个看起来比较有用的东西:nonce ,servertime, pubkey 和 rsakv. 看到pubkey 心中瞬间凉了半截,这厮竟然还是非对称加密。。。 TwT 这种表单去掉外面的括号就是json格式了,使用python 的json库可以方便地读取json数据,这样就不用敲太多恶心的正则表达式出来。

输入密码,登陆.

需要注意的是,登陆过程中出现了好几个跳转所以要开启 preserve log 不然抓到的包就一闪而过了。

当然最重要的应该是紧随prelogin 的那个 POST login.php?client=ssologin.js(v1.4.18) 看一下POST出去的数据是什么:

entry:weibo
gateway:1
from:
savestate:7
useticket:1
pagerefer:http://passport.weibo.com/visitor/visitor?entry=miniblog&a=enter&url=http%3A%2F%2Fweibo.com%2F&domain=.weibo.com&ua=php-sso_sdk_client-0.6.14&_rand=1458639538.8751
vsnf:1
su:Mjk0ODI5MTQ3JTQwcXEuY29t
service:miniblog
servertime:1458642199
nonce:NOZULR
pwencode:rsa2
rsakv:1330428213
sp:1f2a74f7e770la4f81e123bdbc00e13910be389ff0269872a45e71b32cb6df2d9e4b7f308586f11f059b6454
sr:1280*800
encoding:UTF-8
prelt:77
url:http://weibo.com/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack
returntype:META

sp 中的数据我删了一部分,为了防止被黑阔们黑掉账号 = =

不难看出 su = username ; sp = password 。而且password大概就是经过rsa加密过的了。 用python 的rsa 模块验证一下,发现密码明文经过刚才得到的公钥加密之后并不是这个post提交的sp,所以渣浪肯定是对明文的字符串进行了一些混淆处理。虽然我完全不懂js,但是还是要硬着头皮挖掘一下:

首先这个POST的名字是这样的 : login.php?client=ssologin.js(v1.4.18) 而紧随着就是一个叫做 ssologin.js 的东西,当然要看一下他。这个代码长达2300多行,只贴有用的片段。因为已经猜到了她使用RSA进行加密的,而且已经得到了pubkey 这个参数名,用这几个关键词检索一下很快就找到了加密部分的js代码。。如下 :

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

哇哈哈哈哈哈一览无余了。 这里给出了两种加密,不过只看RSA就行了。 注意一下那个10001,这是rsa加密用到的exponent,需要将其(10进制)转换为10进制 ,即65537 以及,我们得到了这样的pattern:

 password = RSAKey.encrypt([me.servertime, me.nonce].join("\t") + "\n" + password)

Almost too easy!

所以对应的python加密代码应该是这个样子的:

import rsa
def get_encrypted_pw(self,password,nonce,,servertime,pub_key):
    rsa_e = 65537 #0x10001 
    pw_string = str(servertime) + '\t' + str(nonce) + '\n' + str(password)
    key = rsa.PublicKey(int(pub_key, 16), rsa_e)
    pw_encypted = rsa.encrypt(pw_string, key)
    return pw_encypted

可是当我们输出这个密码的时候,得到的并不是16进制的样式,而是极其丑陋的 另一种编码。。 所以我们要稍微修改下

passwd = binascii.b2a_hex(pw_encypted)
return passwd 

为了实现这个功能 , 就要用到 binascii模块 ,将二进制转换成ascii/hex。

开始写代码吧!

import所有需要的模块:

import urllib.error
import urllib.request
import urllib.parse
import re
import rsa
import http.cookiejar  #从前的cookielib
import base64
import json
import urllib
import binascii

首先建立一个launcher 类,初始化时接受username 和 password 两个参数。

class Launcher:
    def __init__(self,username,password):
        self.username = username 
        self.password = password 

然后建立一个方法get_encrypted_name,来获取base64加密后的用户名 需要注意的是,用户名中可能包含@这样的符号,而我们刚才看到的加密过的su,解码之后@变成了%40 这其实是url的编码,url把这些特殊字符作为保留字,所以当用到这些字符的时候,会转换成%xx 之类的形式。同样,在这里,加密之前必须先把username字符串转化成url的编码样式,实现这一功能的是urllib.request.qoute 其次,base64编码的对象应该是bytes的形式,而最终我们需要得到string,以完成数据的提交和url的拼接。所以这里用到了str类的.encode() 和 .decode() 方法。

def get_encrypted_name(self):
        username_urllike   = urllib.request.quote(self.username)
        username_encrypted = base64.b64encode(bytes(username_urllike,encoding='utf-8'))
        return username_encrypted.decode('utf-8')

有了base64编码的username,就可以伪造一个request 以获取预登陆的信息 对于返回数据的处理,先用正则表达式扒出一个json,再利用json模块的loads()方法将其包装成一个字典。

def get_prelogin_args(self):
        '''
        该函数用于模拟预登录过程,并获取服务器返回的 nonce , servertime , pub_key 等信息,用一个字典返         回数据
        '''
        json_pattern = re.compile('\((.*)\)')
        url = 'http://login.sina.com.cn/sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack&su=&' + self.get_encrypted_name() + '&rsakt=mod&checkpin=1&client=ssologin.js(v1.4.18)'
        try:
            request = urllib.request.Request(url)
            response = urllib.request.urlopen(request)
            raw_data = response.read().decode('utf-8')
            json_data = json_pattern.search(raw_data).group(1)
            data = json.loads(json_data)
            return data
        except urllib.error as e:
            print("%d"%e.code)
            return None

建立一个 get_encrypted_pw 方法,,利用获取的预登陆信息生成rsa加密过的密码信息,其中必须的pubkey , nonce , rsakv等关键字使用一个字典data 传入

   def get_encrypted_pw(self,data):
        rsa_e = 65537 #0x10001
        pw_string = str(data['servertime']) + '\t' + str(data['nonce']) + '\n' + str(self.password)
        key = rsa.PublicKey(int(data['pubkey'],16),rsa_e)
        pw_encypted = rsa.encrypt(pw_string.encode('utf-8'), key)
        self.password = ''   #安全起见清空明文密码 
        passwd = binascii.b2a_hex(pw_encypted)
        print(passwd)
        return passwd

建立一个cookies容器用于整个登陆过程中的cookies绑定:

def enableCookies(self):
            #建立一个cookies 容器
            cookie_container = http.cookiejar.CookieJar()
            #将一个cookies容器和一个HTTP的cookie的处理器绑定
            cookie_support = urllib.request.HTTPCookieProcessor(cookie_container)
            #创建一个opener,设置一个handler用于处理http的url打开
            opener = urllib.request.build_opener(cookie_support, urllib.request.HTTPHandler)
            #安装opener,此后调用urlopen()时会使用安装过的opener对象
            urllib.request.install_opener(opener)

接下来是一个build_post_data方法,用于包装一个POST方法所需的数据.raw数据来自get_prelogin_args所返回的字典.

def build_post_data(self,raw):
        post_data = {
            "entry":"weibo",
            "gateway":"1",
            "from":"",
            "savestate":"7",
            "useticket":"1",
            "pagerefer":"http://passport.weibo.com/visitor/visitor?entry=miniblog&a=enter&url=http%3A%2F%2Fweibo.com%2F&domain=.weibo.com&ua=php-sso_sdk_client-0.6.14",
            "vsnf":"1",
            "su":self.get_encrypted_name(),
            "service":"miniblog",
            "servertime":raw['servertime'],
            "nonce":raw['nonce'],
            "pwencode":"rsa2",
            "rsakv":raw['rsakv'],
            "sp":self.get_encrypted_pw(raw),
            "sr":"1280*800",
            "encoding":"UTF-8",
            "prelt":"77",
            "url":"http://weibo.com/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack",
            "returntype":"META"
        }
        data = urllib.parse.urlencode(post_data).encode('utf-8')
        return data

万事具备,尝试登陆吧(其实这一步很恶心。。。)

def login(self):
        url = 'http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.18)'
        self.enableCookies()
        data = self.get_prelogin_args()
        post_data = self.build_post_data(data)
        headers = {
            "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
        }
        try:
            request = urllib.request.Request(url=url,data=post_data,headers=headers)
            response = urllib.request.urlopen(request)
            html = response.read().decode('GBK')
            '''
            一开始用的是utf-8解码,然而得到的数据很丑陋,却隐约看见一个GBK字样。所以这里直接采用GBK解码
            '''
            print(html)
        except urllib.error as e:
            print(e.code)

返回的是啥呢 ? 让我萌看一下

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


<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&savestate=1490380154","http:\/\/passport.97973.com\/sso\/crossdomain?action=login&savestate=1490380154","http:\/\/passport.weibo.cn\/sso\/crossdomain?action=login&savestate=1"]});}
		catch(e){
			var msg = e.message;
			var img = new Image();
			var type = 1;
			img.src = 'http://login.sina.com.cn/sso/debuglog?msg=' + msg +'&type=' + type;
		}try{sinaSSOController.crossDomainAction('login',function(){location.replace('http://passport.weibo.com/wbsso/login?ssosavestate=1490380154&url=http%3A%2F%2Fweibo.com%2Fajaxlogin.php%3Fframelogin%3D1%26callback%3Dparent.sinaSSOController.feedBackUrlCallBack&ticket=ST-Mjc3MzI1MDM3MQ==-1458844154-gz-71D924714FB4757A29087C4F732C1B20&retcode=0');});}
		catch(e){
			var msg = e.message;
			var img = new Image();
			var type = 2;
			img.src = 'http://login.sina.com.cn/sso/debuglog?msg=' + msg +'&type=' + type;
		}
</script>
</body>
</html>
http://passport.weibo.com/wbsso/login?ssosavestate=1490380154&url=http%3A%2F%2Fweibo.com%2Fajaxlogin.php%3Fframelogin%3D1%26callback%3Dparent.sinaSSOController.feedBackUrlCallBack&ticket=ST-Mjc3MzI1MDM3MQ==-1458844154-gz-71D924714FB4757A29087C4F732C1B20&retcode=0

结合抓包得到的302状态码,不难发现这是一段重定向代码,重定向的url写在 location.replace的后面,所以我萌编写一个正则表达式,把这段url扒下来

        p = re.compile('location\.replace\(\'(.*?)\'\)')
        try:
            login_url = p.search(html).group(1)
            print(login_url)
            request = urllib.request.Request(login_url)
            response = urllib.request.urlopen(request)
            page = response.read().decode('utf-8')
            print(page)

这下总能登陆进去了吧。。。拿衣服!

我们得到的是:


<html><head><script language='javascript'>parent.sinaSSOController.feedBackUrlCallBack({"result":true,"userinfo":{"uniqueid":"2773250371","userid":null,"displayname":null,"userdomain":"?wvr=5&lf=reg"}});</script></head><body></body></html>

尼玛,又是一个重定向啊?! 然而这次很轻易地注意到里面有个"?wvr=5&lf=reg"字段肥肠眼熟,看看刚才手工登陆抓到的包,果然,这是最终链接的一部分。 所以再搞一个正则表达式,把他也搞出来,然后拼接一个最终url出来,就可以轻松而愉悦地模拟登陆了!

于是就有了最终的全部代码。。

import urllib.error
import urllib.request
import re
import rsa
import http.cookiejar  #从前的cookielib
import base64
import json
import urllib
import urllib.parse
import binascii

# 用于模拟登陆新浪微博

class launcher():

    def __init__(self,username, password):
        self.password = password
        self.username = username

    def get_prelogin_args(self):

        '''
        该函数用于模拟预登录过程,并获取服务器返回的 nonce , servertime , pub_key 等信息
        '''
        json_pattern = re.compile('\((.*)\)')
        url = 'http://login.sina.com.cn/sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack&su=&' + self.get_encrypted_name() + '&rsakt=mod&checkpin=1&client=ssologin.js(v1.4.18)'
        try:
            request = urllib.request.Request(url)
            response = urllib.request.urlopen(request)
            raw_data = response.read().decode('utf-8')
            json_data = json_pattern.search(raw_data).group(1)
            data = json.loads(json_data)
            return data
        except urllib.error as e:
            print("%d"%e.code)
            return None

    def get_encrypted_pw(self,data):
        rsa_e = 65537 #0x10001
        pw_string = str(data['servertime']) + '\t' + str(data['nonce']) + '\n' + str(self.password)
        key = rsa.PublicKey(int(data['pubkey'],16),rsa_e)
        pw_encypted = rsa.encrypt(pw_string.encode('utf-8'), key)
        self.password = ''   #清空password
        passwd = binascii.b2a_hex(pw_encypted)
        print(passwd)
        return passwd

    def get_encrypted_name(self):
        username_urllike   = urllib.request.quote(self.username)
        username_encrypted = base64.b64encode(bytes(username_urllike,encoding='utf-8'))
        return username_encrypted.decode('utf-8')

    def enableCookies(self):
            #建立一个cookies 容器
            cookie_container = http.cookiejar.CookieJar()
            #将一个cookies容器和一个HTTP的cookie的处理器绑定
            cookie_support = urllib.request.HTTPCookieProcessor(cookie_container)
            #创建一个opener,设置一个handler用于处理http的url打开
            opener = urllib.request.build_opener(cookie_support, urllib.request.HTTPHandler)
            #安装opener,此后调用urlopen()时会使用安装过的opener对象
            urllib.request.install_opener(opener)

    def build_post_data(self,raw):
        post_data = {
            "entry":"weibo",
            "gateway":"1",
            "from":"",
            "savestate":"7",
            "useticket":"1",
            "pagerefer":"http://passport.weibo.com/visitor/visitor?entry=miniblog&a=enter&url=http%3A%2F%2Fweibo.com%2F&domain=.weibo.com&ua=php-sso_sdk_client-0.6.14",
            "vsnf":"1",
            "su":self.get_encrypted_name(),
            "service":"miniblog",
            "servertime":raw['servertime'],
            "nonce":raw['nonce'],
            "pwencode":"rsa2",
            "rsakv":raw['rsakv'],
            "sp":self.get_encrypted_pw(raw),
            "sr":"1280*800",
            "encoding":"UTF-8",
            "prelt":"77",
            "url":"http://weibo.com/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack",
            "returntype":"META"
        }
        data = urllib.parse.urlencode(post_data).encode('utf-8')
        return data


    def login(self):
        url = 'http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.18)'
        self.enableCookies()
        data = self.get_prelogin_args()
        post_data = self.build_post_data(data)
        headers = {
            "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
        }
        try:
            request = urllib.request.Request(url=url,data=post_data,headers=headers)
            response = urllib.request.urlopen(request)
            html = response.read().decode('GBK')
            #print(html)
        except urllib.error as e:
            print(e.code)

        p = re.compile('location\.replace\(\'(.*?)\'\)')
        p2 = re.compile(r'"userdomain":"(.*?)"')

        try:
            login_url = p.search(html).group(1)
            print(login_url)
            request = urllib.request.Request(login_url)
            response = urllib.request.urlopen(request)
            page = response.read().decode('utf-8')
            print(page)
            login_url = 'http://weibo.com/' + p2.search(page).group(1)
            request = urllib.request.Request(login_url)
            response = urllib.request.urlopen(request)
            final = response.read().decode('utf-8')

            print("Login success!")
        except:
            print('Login error!')
            return 0