模拟新浪微博登录-原理分析到实现

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了模拟新浪微博登录-原理分析到实现相关的知识,希望对你有一定的参考价值。

原文地址:http://www.csuldw.com/2016/11/10/2016-11-10-simulate-sina-login/

 

上一篇文章 小试牛刀:使用Python模拟登录知乎 介绍了如何模拟知乎登录,虽然用到了验证码信息,但请求的参数都是原封不动的传递,刚开始接触的时候,觉得难度适中,回头再看的时候,反而感觉挺容易的。在这篇文章,将继续介绍模拟登录。与之前不一样的是,这次选择的对象是新浪微博,难度稍微提升了点,好在以往的许多码友们都留有许多经验贴,经过几番斟酌,微博的模拟登录算是实现了。这两天还在研究如何高性能地爬取微博数据,业余之际乘着还有点记忆,索性将先前的小实验加工成文,算是一份小结吧。下面来看看整个实验过程。

开发工具

一如既往,笔者使用的还是之前的工具,如下:

  • Windows 7 + Python 2.75
  • Chrome + Fiddler

微博登录请求过程分析

新浪微博的登录有多个URL链接,笔者在实验的时候试了两个,这两个都是新浪通行证登录页面,都是不需要验证码的。一个是 【 http://login.sina.com.cn 】,另一个是 【 https://login.sina.com.cn/signup/signin.php?entry=sso 】。两个URL虽然很大部分相同,登录过程中仅仅是传递参数不一样。第一个URL传递的过程对password进行了加密,而第二个没有加密,所以如果使用第二个URL进行模拟登录,就简单多了。在这里,笔者决定选择使用第一种方式进行分析,下面来看详细过程。

请求登录过程主要分三部分

  1. 请求登录login.php页面前的参数获取
  2. 请求登录login.php页面时的参数分析
  3. 提交POST请求时的参数

Step 1:GET方式请求prelogin.php页面

在模拟登录之前,先观察浏览器登录过程中Fiddler抓到的包,在 /sso/login.php打开之前会先使用 GET 方式请求 /sso/prelogin.php ,请求的URL为:【 https://login.sina.com.cn/sso/prelogin.php?entry=account&callback=sinaSSOController.preloginCallBack&su=bGl1ZGl3ZWkxOCU0MHNpbmEuY29t&rsakt=mod&client=ssologin.js(v1.4.15) 】,可以看看下面这张图:

技术分享

在Fiddler中,可以点击 Preview 查看具体详情,也可以直接将Request URL复制到浏览器上查看,效果图如下:

技术分享

可以看出,这是一个json数据,并且携带了几个参数,我们关心的有以下四个:

  • servertime
  • nonce
  • pubkey
  • rsakv

说明一下,之所以认为这几个参数比较重要,那是因为后面对 password 的加密需要用到,对其他参数没有提及的原因是在提交POST时其它的参数并没有用到。好了,为了进行进一步探索,我们从Fiddler的结果可以看出,接下来到了 /sso/login.php 。

Step 2:POST方式请求login.php页面

从这里开始,就进行 login.php 页面的请求了(详细的Request URL:【 https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.15) 】,后面的时间戳可省略)。点击查看详情,结果图如下:

技术分享

可以发现 /sso/login.php 页面有如下参数(From Data):

cdult: 3
domain: sina.com.cn
encoding: UTF-8
entry: account
from:
gateway: 1
nonce: AFE3O9
pagerefer: http://login.sina.com.cn/sso/logout.php
prelt: 41
pwencode: rsa2
returntype: TEXT
rsakv: 1330428213
savestate: 30
servertime: 1478568922
service: sso
sp: password
sr: 1366*768
su: username
useticket: 0
vsnf: 1

到了这里,我们大概可以知道我们需要哪些参数了。在From Data 参数列表中,需要我们指定的参数有下面几个:

  • servertime
  • nonce
  • rsakv
  • sp:加密后的密码
  • su:加密后的用户名

对于参数 nonce 、 servertime 、 rsakv ,都可以从第一步中的 prelogin.php中直接获取,而 sp 和 su 则是经过加密后的字符串值,至于具体的加密规则,我们下面通过查看源码分析得出。

Step 3:探索加密规则

首先看看请求 /sso/prelogin.php 的具体情况,看到 client 为 ssologin.js ,

技术分享

然后我们到登录页面 https://login.sina.com.cn 中查看源码【 view-source:https://login.sina.com.cn/ 】并搜索“ssllogin.js”,接着点击进入 ssologin.js ,搜索“username”字符串,找到与“username”相应的加密部分(这里需仔细查看+揣测),然后搜索“password”,找到“password”的加密部分,最后分析出“username”和“password”的加密规则。加密部分如下:

技术分享

加密用户名的代码:

request.su = sinaSSOEncoder.base64.encode(urlencode(username));

加密密码的代码:

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)
  }
}

微博对于 username 的加密规则比较单一,使用的是 Base64 加密算法,而对 password 的加密规则比较复杂,虽然使用的是 RSA2 (python中需要使用 pip install rsa 安装rsa模块),但加密的逻辑比较多。根据上面的代码,可以看出 password 加密是这样的一个过程:首先创建一个 rsa 公钥,公钥的两个参数都是固定值,第一个参数是登录过程中 prelogin.php 中的 pubkey ,第二个参数是加密的 js 文件中指定的”10001”(这两个值需要先从16进制转换成10进制,把“10001”转成十进制为“65537”)。最后再加入 servertime 和 nonce 进行进一步加密。

经过上面的分析之后,发起 POST 请求时的 post_data 基本上已经全部可以得到了,接下来就跟模拟登录其它网站类似了,可以使用 request ,也可以使用 urllib2。下面来看详细代码部分。

源码实现

Github源码链接: https://github.com/csuldw/WSpider/tree/master/SinaLogin,源码包括下列文件:

  • dataEncode.py:用于对提交POST请求的数据进行编码处理
  • Logger.py:用于打印log
  • SinaSpider.py:用于爬取sina微博数据的文件(主文件)

为了方便扩展,笔者将代码进行了封装,所以看起来代码量比较多,不过个人觉得可读性还是比较良好,算是凑合吧。

1. dataEncode.py

# -*- coding: utf-8 -*-
"""
Created on Tue Nov 08 10:14:38 2016

@author: liudiwei
"""
import base64
import rsa
import binascii
import requests
import json
import re

#使用base64对用户名进行编码
def encode_username(username):
    return base64.encodestring(username)[:-1]
    
#使用rsa2对password进行编码
def encode_password(password, servertime, nonce, pubkey):
    rsaPubkey = int(pubkey, 16)
    RSAKey = rsa.PublicKey(rsaPubkey, 65537) #创建公钥
    codeStr = str(servertime) + ‘\t‘ + str(nonce) + ‘\n‘ + str(password) #根据js拼接方式构造明文
    pwd = rsa.encrypt(codeStr, RSAKey)  #使用rsa进行加密
    return binascii.b2a_hex(pwd)  #将加密信息转换为16进制。

#读取preinfo.php,获取servertime, nonce, pubkey, rsakv四个参数值
def get_prelogin_info():
    url = r‘http://login.sina.com.cn/sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack&su=&rsakt=mod&client=ssologin.js(v1.4.18)‘
    html = requests.get(url).text
    jsonStr = re.findall(r‘\((\{.*?\})\)‘, html)[0]
    data = json.loads(jsonStr)
    servertime = data["servertime"]
    nonce = data["nonce"]
    pubkey = data["pubkey"]
    rsakv = data["rsakv"]
    return servertime, nonce, pubkey, rsakv

#根据Fiddler抓取的数据,构造post_data
def encode_post_data(username, password, servertime, nonce, pubkey, rsakv):
    su = encode_username(username)
    sp = encode_password(password, servertime, nonce, pubkey)
    #用于登录到 http://login.sina.com.cn
    post_data = {
        "cdult" : "3",
        "domain" : "sina.com.cn",
        "encoding" : "UTF-8",
        "entry" : "account",
        "from" : "",
        "gateway" : "1",
        "nonce" : nonce,
        "pagerefer" : "http://login.sina.com.cn/sso/logout.php",
        "prelt" : "41",
        "pwencode" : "rsa2",
        "returntype" : "TEXT",
        "rsakv" : rsakv,
        "savestate" : "30",
        "servertime" : servertime,
        "service" : "sso",
        "sp" : sp,
        "sr" : "1366*768",
        "su" : su,
        "useticket" : "0",
        "vsnf" : "1"
    }
    #用于登录到 http://login.sina.com.cn/signup/signin.php?entry=ss,将POST替换成下面的即可
    """
    post_data = {
        "cdult" : "3",
        "domain" : "sina.com.cn",
        "encoding" : "UTF-8",
        "entry" : "sso",
        "from" : "null",
        "gateway" : "1",
        "pagerefer" : "",
        "prelt" : "0",
        "returntype" : "TEXT",
        "savestate" : "30",
        "service" : "sso",
        "sp" : password,
        "sr" : "1366*768",
        "su" : su,
        "useticket" : "0",
        "vsnf" : "1"
    }    
    """
    return post_data

2. Logger.py

# -*- coding: utf-8 -*-
"""
Created on Thu Nov 02 14:01:17 2016

@author: liudiwei
"""
import os
import logging  

class LogClient(object):
    def __init__(self):
        self.logger = None

    """#EXAMPLE 
    logger = createLogger(‘mylogger‘, ‘temp/logger.log‘)
    logger.debug(‘logger debug message‘)  
    logger.info(‘logger info message‘)  
    logger.warning(‘logger warning message‘)  
    logger.error(‘logger error message‘)  
    logger.critical(‘logger critical message‘)  
    """    
    def createLogger(self, logger_name, log_file):
        prefix = os.path.dirname(log_file)
        if not os.path.exists(prefix):
            os.makedirs(prefix)
        # 创建一个logger
        logger = logging.getLogger(logger_name)  
        logger.setLevel(logging.INFO)  
        # 创建一个handler,用于写入日志文件    
        fh = logging.FileHandler(log_file)  
        # 再创建一个handler,用于输出到控制台    
        ch = logging.StreamHandler()  
        # 定义handler的输出格式formatter    
        formatter = logging.Formatter(‘%(asctime)s | %(name)s | %(levelname)s | %(message)s‘)  
        fh.setFormatter(formatter)  
        ch.setFormatter(formatter)  
        # 给logger添加handler    
        logger.addHandler(fh)  
        logger.addHandler(ch)
        self.logger = logger
        return self.logger

2. SinaSpider.py

# -*- coding: utf-8 -*-
"""
Created on Tue Nov 08 10:14:38 2016

@author: liudiwei
"""
import os
import getpass
import json
import requests
import cookielib
import urllib
import urllib2
import gzip
import StringIO
import time

import dataEncode
from Logger import LogClient

class SinaClient(object):
    def __init__(self, username=None, password=None):
        #用户输入的用户名与密码
        self.username = username
        self.password = password
        #从prelogin.php中获取的数据
        self.servertime = None
        self.nonce = None
        self.pubkey = None
        self.rsakv = None
        #请求时提交的数据列表
        self.post_data = None
        self.headers = {}
        #用于存储登录后的session
        self.session = None   
        self.cookiejar = None
        #用于输出log信息
        self.logger = None
        #存储登录状态,初始状态为False        
        self.state = False
        #初始时调用initParams方法,初始化相关参数
        self.initParams()
    
    #初始化参数
    def initParams(self):
        self.logger = LogClient().createLogger(‘SinaClient‘, ‘out/log_‘ + time.strftime("%Y%m%d", time.localtime()) + ‘.log‘)
        self.headers = dataEncode.headers
        return self
    
    #设置username 和 password
    def setAccount(self, username, password):
        self.username = username
        self.password = password
        return self
    
    #设置post_data
    def setPostData(self):
        self.servertime, self.nonce, self.pubkey, self.rsakv = dataEncode.get_prelogin_info()
        self.post_data = dataEncode.encode_post_data(self.username, self.password, self.servertime, self.nonce, self.pubkey, self.rsakv)
        return self
        
    #使用requests库登录到 https://login.sina.com.cn
    def login(self, username=None, password=None):
        #根据用户名和密码给默认参数赋值,并初始化post_data
        self.setAccount(username, password) 
        self.setPostData()
        #登录时请求的url
        login_url = r‘https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.15)‘
        session = requests.Session()
        response = session.post(login_url, data=self.post_data)
        json_text = response.content.decode(‘gbk‘)
        res_info = json.loads(json_text)
        try:
            if res_info["retcode"] == "0":
                self.logger.info("Login success!")
                self.state = True
                #把cookies添加到headers中
                cookies = session.cookies.get_dict()
                cookies = [key + "=" + value for key, value in cookies.items()]
                cookies = "; ".join(cookies)
                session.headers["Cookie"] = cookies
            else:
                self.logger.error("Login Failed! | " + res_info["reason"])
        except Exception, e:
            self.logger.error("Loading error --> " + e)
        self.session = session
        return session
    
    #生成Cookie,接下来的所有get和post请求都带上已经获取的cookie
    def enableCookie(self, enableProxy=False):
        self.cookiejar = cookielib.LWPCookieJar()  # 建立COOKIE
        cookie_support = urllib2.HTTPCookieProcessor(self.cookiejar)
        if enableProxy:
            proxy_support = urllib2.ProxyHandler({‘http‘: ‘http://122.96.59.107:843‘}) # 使用代理
            opener = urllib2.build_opener(proxy_support, cookie_support, urllib2.HTTPHandler)
            self.logger.info("Proxy enable.")
        else:
            opener = urllib2.build_opener(cookie_support, urllib2.HTTPHandler)
        urllib2.install_opener(opener)
    
    #使用urllib2模拟登录过程
    def login2(self, username=None, password=None):
        self.logger.info("Start to login...")
        #根据用户名和密码给默认参数赋值,并初始化post_data
        self.setAccount(username, password) 
        self.setPostData()
        self.enableCookie()
        #登录时请求的url
        login_url = r‘https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.15)‘
        headers = self.headers
        request = urllib2.Request(login_url, urllib.urlencode(self.post_data), headers)
        resText = urllib2.urlopen(request).read()
        try:        
            jsonText = json.loads(resText)
            if jsonText["retcode"] == "0":
                self.logger.info("Login success!")
                self.state = True
                #将cookie加入到headers中
                cookies = ‘;‘.join([cookie.name + "=" + cookie.value for cookie in self.cookiejar])
                headers["Cookie"] = cookies
            else:
                self.logger.error("Login Failed --> " + jsonText["reason"])
        except Exception, e:
            print e
        self.headers = headers
        return self
    
    #打开url时携带headers,此header需携带cookies
    def openURL(self, url, data=None):
        req = urllib2.Request(url, data=data, headers=self.headers)
        text = urllib2.urlopen(req).read()
        return self.unzip(text)
    
    #功能:将文本内容输出至本地
    def output(self, content, out_path, save_mode="w"):
        self.logger.info("Download html page to local machine. | path: " + out_path)
        prefix = os.path.dirname(out_path)
        if not os.path.exists(prefix):
            os.makedirs(prefix)
        fw = open(out_path, save_mode)
        fw.write(content)
        fw.close()
        return self
        
    """
    防止读取出来的HTML乱码,测试样例如下
    req = urllib2.Request(url, headers=headers)
    text = urllib2.urlopen(req).read()
    unzip(text)
    """
    def unzip(self, data):
        data = StringIO.StringIO(data)
        gz = gzip.GzipFile(fileobj=data)
        data = gz.read()
        gz.close()
        return data    

#调用login1进行登录
def testLogin():
    client = SinaClient()
    username = raw_input("Please input username: ")
    password = getpass.getpass("Please input your password: ")   
    session = client.login(username, password)
    
    follow = session.post("http://weibo.cn/1669282904/follow").text.encode("utf-8")
    client.output(follow, "out/follow.html")


#调用login2进行登录
def testLogin2():
    client = SinaClient()
    username = raw_input("Please input username: ")
    password = getpass.getpass("Please input your password: ")   
    session = client.login2(username, password)
    
    info = session.openURL("http://weibo.com/1669282904/info")
    client.output(info, "out/info2.html")    
    
if __name__ == ‘__main__‘:
    testLogin2()

关于源码的分析,可以参考代码中的注解,如有不理解的地方,可在评论中提出。

运行

直接在Windows控制台运行 python SinaSpider.py ,然后根据提示输入用户名和密码即可。

运行结果展示

技术分享

OK,可以成功的登录到微博了,接下来想爬取什么数据就尽情的爬吧。

 

更正一个我在运行时发现的问题:

password无法输入,将testLogin1和testLogin2中输入密码的

password = getpass.getpass("Please input your password: ")

改为  password = raw_input("Please input your password: ") 

以上是关于模拟新浪微博登录-原理分析到实现的主要内容,如果未能解决你的问题,请参考以下文章

定向爬虫 - Python模拟新浪微博登录

定向爬虫 - Python模拟新浪微博登录

求真正有效的可以模拟登录新浪微博的java代码,后续可以用Jsoup进行抓取。急急!!登录成功马上给分!

全程模拟新浪微博登录(2015)

Node.js-Koa2框架生态实战-从零模拟新浪微博

python网络编程使用rsa加密算法模块模拟登录新浪微博