• requests模拟登录12306


    1、开局闲聊

    昨天下午准备随便找找高铁票时,突然对12306的登录产生了兴趣,于是研究了一下,先说明两点:

    • 验证码
      这部分调用的是一个大佬现成的API,如果各位大佬对识别验证码部分感兴趣,我这个小菜鸡很抱歉帮不上忙
    • 登录表单
      实际上12306登录的表单十分简单,字段少JS代码基本没什么难度,所以想看高阶JS逆向的姐姐可能要失望了

    既然没什么难点,那有人可能就问了为什么还要水一篇文章?

    原因就是12306登录过程中还是有不少坑,虽然单个页面请求字段少,但顶不住一个登录请求居然有十来个请求页面,在这个过程中各种自动手动设置Cookie,如果你憨憨的只请求登录url是永远失败的(别问我怎么知道的!),浪费我好几个小时时间。

    2、准备工具

    • 浏览器开发者工具
    • Fiddler或Charles等抓包工具
      建议使用抓包工具,因为请求太多时控制台不是很直观,控制台主要用于JS调试,虽然登录12306基本不需要用到调试

    3、请求分析

    首先运行抓包工具监听,在浏览器打开12306的主页手动登录一次,登录成功后就可以抓包监听了,我这里用的是Charles

    显示的结果有很多,但是不要怕,大部分都是静态文件,除掉静态文件其他相关请求大概也就十五六个...
    我一开始被N次登录失败搞上头了总觉得12306在请求上埋坑于是还真按着这十多个请求一路写下去...
    后来想想其实没必要这么做,关注主要的几个登录请求中的Cookie字段来源再针对性发出请求就好了,事后也证明只需要经过6个请求,就能成功登录。
    所以本文我会着手从关键的几个登录请求分析,就不赘述摸索过程的痛了。

    3.1 获取验证码

    生成验证码的URL很容易找到

    https://kyfw.12306.cn/passport/captcha/captcha-image64?login_site=E&module=login&rand=sjrand&1589797925252&callback=jQuery191020097810026343454_1589797924079&_=1589797924080

    请求字段

    请求字段基本上都是固定值,callback和_还有1589797925252是和时间戳相关的,但服务器压根不验证callback和_,所以我们不用管
    Cookie和响应

    响应image就是经过base64验证码图片,验证码图片从这里就可以拿到了
    重点说Cookie,虽然字段有点多,但其实只要关注使用JS手动设置的字段就可以了,因为Session会帮我们处理响应头里的Cookie,重点就是确定哪些字段不是通过响应头设置的,可以直接搜索字段,比如第一个_passport_session

    结果显示在某一个请求的响应头中存在Set-Cookie: _passport_session 那我们就可以暂时放下不管
    再比如RAIL_EXPIRATION字段

    在一个JS文件中出现了该字段,这表明是手动添加的,需要重点关注,既然是手动添加的字段要么是JS计算生成要么是服务器响应,可以先找找字段值

    成功在响应中找到了值,点进去看看

    首先分析响应有三个字段exp、cookieCode和dfp,经比对exp和dfp就是验证码请求Cookie中的RAIL_EXPIRATION和RAIL_DEVICEID
    其次看看这个请求本身:

    https://kyfw.12306.cn/otn/HttpZF/logdevice?algID=y6fvmhGlLP&hashCode=WRXET1wCtYsDWujgBBDiq2A4aqJOy-G6t5VK5OI0wNY&FMQw=0&q4f3=zh-CN&VPIf=1&custID=133&VEek=unknown&dzuS=0&yD16=0&EOQP=c227b88b01f5c513710d4b9f16a5ce52&jp76=52d67b2a5aa5e031084733d5006cc664&hAqN=MacIntel&platform=WEB&ks0Q=d22ca0b81584fbea62237b14bd04c866&TeRS=709x1280&tOHY=24xx800x1280&Fvje=i1l1o1s1&q5aJ=-8&wNLf=99115dfb07133750ba677d055874de87&0aew=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36&E3gR=92271eade53193a7130e280652b8e939&timestamp=1589810100160

    请求参数虽然有很多,但浏览器打开是可以直接响应结果的,所以暂且不管它,实际上这些参数的生成过程还是有点意思的,过两天我会专门写一篇分析这部分JS参数的文章,现在我们直接使用访问拿到的结果就能通过
    回到获取验证码的请求cookie上来,经过分析发现除开RAIL_EXPIRATION和RAIL_DEVICEID其他字段都是响应头设置的,所以在获取验证码图片这一步我们只需要进行两个请求:

    import re
    import requests
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36',
        'Referer': 'https://kyfw.12306.cn/otn/resources/login.html',
        'Host': 'kyfw.12306.cn'
    }
    
    session = requests.Session()
    # p1这些字段其实基本就是获取设备信息再进行加密处理
    p1 = {
        'algID': 'y6fvmhGlLP',
        'hashCode': 'WRXET1wCtYsDWujgBBDiq2A4aqJOy-G6t5VK5OI0wNY',
        'FMQw': 0,
        'q4f3': 'zh-CN',
        'VPIf': 1,
        'custID': 133,
        'VEek': 'unknown',
        'dzuS': 0,
        'yD16': 0,
        'EOQP': 'c227b88b01f5c513710d4b9f16a5ce52',
        'jp76': '52d67b2a5aa5e031084733d5006cc664',
        'hAqN': 'MacIntel',
        'platform': 'WEB',
        'ks0Q': 'd22ca0b81584fbea62237b14bd04c866',
        'TeRS': '709x1280',
        'tOHY': '24xx800x1280',
        'Fvje': 'i1l1o1s1',
        'q5aJ': '-8',
        'wNLf': '99115dfb07133750ba677d055874de87',
        '0aew': headers['User-Agent'],
        'E3gR': '92271eade53193a7130e280652b8e939',
        'timestamp': int(time.time() * 1000)
    }
    # 第一次请求获取exp和dfp
    r1 = session.get('https://kyfw.12306.cn/otn/HttpZF/logdevice', params=p1, headers=headers)
    exp = re.search(r'exp":"(d+)",', r1.text).group(1)
    dfp = re.search(r'dfp":"(.+?)"', r1.text).group(1)
    cookieCode = re.search(r'cookieCode":"(.+?)"', r1.text).group(1)    # 这个字段暂时不确定作用
    session.cookies.update({'RAIL_DEVICEID': dfp, 'RAIL_EXPIRATION': exp})  # 手动添加cookie
    # 第二次请求获取验证码
    p2 = {
        'login_site': 'E',
        'module': 'login',
        'rand': 'sjrand',
        str(int(time.time() * 1000)): ''
    }
    r2 = session.get('https://kyfw.12306.cn/passport/captcha/captcha-image64', params=p2)
    image = re.search(r'image":"(.+?)",', r2.text).group(1)
    

    验证码图片就成功拿到了

    3.2 提交验证结果


    如上图,在点击登录后的第一步是发起一个ajax请求判断验证码是否正确
    老规矩,上URL

    https://kyfw.12306.cn/passport/captcha/captcha-check?callback=jQuery19108610389300189387_1589810101978&answer=197%2C125%2C26%2C129&rand=sjrand&login_site=E&_=1589810101981

    先分析请求参数,发现除了answer也就是验证码答案是动态的,其他都是静态的,那这个答案197,125,26,129是怎么整出来的呢,这时候用浏览器分析页面结构:

    先随便在验证码图片上点一下,然后右键检查元素

    发现当在图片上标记一个点,页面就会生成一个div标签,很简单就能推测出randcode就是提交答案,它的数值就是图片所在的平面位置点,我们可以随意的在8个图中取一个点作为第几张图的映射,如

    position = {
        1: '49,48', 2: '124,52', 3: '200,43', 4: '259,47',
        5: '50,113', 6: '101,102', 7: '198,112', 8: '250,127'
    }
    
    def getVerifyResult(path: str):
        """调用API接口获取验证码结果
        :param path: 验证码图片路径
        :return:
        """
        url = "http://littlebigluo.qicp.net:47720/"
        ret = []
        # 发送post请求把图片数据带上
        file = open(path, 'rb')
        response = requests.post(url, data={"type": "1",},
                                 files={'pic_xxfile': file})
        file.close()
        # 返回识别结果
        for i in re.findall("<B>(.*)</B>", response.text)[0].split(" "):
            ret.append(position[int(i)])
        return ret
    

    接下来调用上面的识别函数获取答案

    import base64
    code_path = "code.jpg"
    imgdata = base64.b64decode(image)
    with open(code_path, 'wb') as f:
        f.write(imgdata)
    capchat = getVerifyResult(code_path)
    answer = ','.join(capchat)
    


    Cookie和响应,都是老东西中规中矩没有需要注意的

    在提交验证结果这一步,只需要一个请求

    p3 = {
        'answer': answer,
        'rand': 'sjrand',
        'login_site': 'E'
    }
    # 第三次请求 提交验证码答案
    r3 = session.get('https://kyfw.12306.cn/passport/captcha/captcha-check', headers=headers, params=p3)
    j3 = r3.json()
    if j3.get('result_message') != '验证码校验成功':
        raise RuntimeError('【验证码异常】: ' + j3.get('result_message'))
    

    3.3 提交登录表单


    URL:

    https://kyfw.12306.cn/passport/web/login

    表单字段就4个,账号密码都是明文,answer是上一步的验证码答案
    响应结果是一段json,其中返回了一段比较长的字段uamtk,先记下来暂且不管

    r4 = session.post('https://kyfw.12306.cn/passport/web/login',
                          data={'username': user,
                                      'password': password,
                                      'appid': 'on',
                                      'answer': answer},
                          headers=headers)
    j4 = r4.json()
    if j4.get('result_message') != '登录成功':
        raise RuntimeError('【登录失败】: ' + j4.get('result_message'))
    uamtk = j4['uamtk']
    

    虽然返回登录成功,但你可千万别天真的相信了,这个cookie依然没通过认证

    有同学可能注意到login后面紧贴的2个请求,但经验证这两个请求可有可无,从它们不涉及生产任何cookie也可以验证这一点

    3.4 登录验证

    继续寻找请求记录,发现了一个显眼的URL请求

    https://kyfw.12306.cn/passport/web/auth/uamtk


    注意到Cookie里面出现的新字段uamtk就是login返回的uamtk,可以直接安排上
    响应也有一串字符串newapptk,老规矩记下来再说
    接下来看看提交的表单

    哟,又是白给,可以很轻松的构建这个请求

    r5 = session.post('https://kyfw.12306.cn/passport/web/auth/uamtk',
                            headers={'User-Agent': headers['User-Agent'],
                                     'Referer': 'https://kyfw.12306.cn/otn/passport?redirect=/otn/login/userLogin',
                                     'Origin': 'https://kyfw.12306.cn',
                                     'Host': 'kyfw.12306.cn'},
                            data={'appid': 'otn'})
    j5 = r5.json()
    if j5.get('result_message') != '验证通过':
        raise RuntimeError('【验证失败】: ', j5.get('result_message'))
    tk = j5['newapptk']
    

    此时此刻依然还未通过验证,继续找下去

    url:

    https://kyfw.12306.cn/otn/uamauthclient

    为什么会找到它呢因为响应很显眼啊,都返回用户名了重点关注准没错
    可以看到表单一个字段tk,不就是上一个请求给我们的newapptk嘛
    再看Cookie

    没有新鲜玩意,直接构建请求就完事了

    r6 = session.post('https://kyfw.12306.cn/otn/uamauthclient',
                           data={'tk': tk},
                           headers=headers)
    j6 = r6.json()
    if j6.get('result_message') != '验证通过':
        raise RuntimeError('【验证失败】: ', j6.get('result_message'))
    

    这就是最后一个请求了,没有更多了...

    3.5 验证登录是否成功

    经过前面的6个请求,我们的session已经可以使用了,由于12306的信息基本用ajax获取,所以访问普通页面是得不到信息的,这里可以使用一个api来判断是否登录成功:

    https://kyfw.12306.cn/otn/index/initMy12306Api

    url = 'https://kyfw.12306.cn/otn/index/initMy12306Api'
    r = session.post(url)
    if '"status":true' in r.text:
        print('登录成功:', r.json()['data']['user_name'])
    else:
        raise RuntimeError('登录失败...')
    

    至此,模拟登录就完成了,cookie可以保留到本地,下次登录直接使用cookie就不用这么折腾了。

    4、完整代码

    我把整个模拟登录代码封装好了,目前只实现了模拟登录并上传到了Github,有兴趣的小伙伴可以去看看,如果对你有帮助欢迎点个Star

  • 相关阅读:
    vue less使用/(除号/斜杠),浏览器不识别问题
    antDesignOfVue 表格复选框添加了默认选中状态,点击清空后数据清空,复选框的选中状态不变的问题。
    vue 可多选的日期插件
    antDesignOfVue 走马灯组件不自动定位到第一页
    antDesignOfVue 表格固定列之后出现空白列
    vue 动态添加表格列
    antDesignOfVue 符合条件的表格复选框禁止选中
    更换文件名大小写之后,git远程文件还存在,删除远程仓库的文件/文件夹
    git文件名大小写不敏感,更改文件大小写后无反应。
    记:mysql查询列含中文
  • 原文地址:https://www.cnblogs.com/lazyfish007/p/12913874.html
Copyright © 2020-2023  润新知