非常久之前就了解过模拟登录的过程。近期对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的问题等等。

欢迎交流和指正~_~