当前位置: 首页 > news >正文

记一次用 NodeJs 实现模拟登录的思路

记一次用 NodeJs 实现模拟登录的思路

作者 @zwhu
原文章 @github

工欲善其事,必先利其器。

给自己定下写文章的目标后,就去找了几家博客平台来发布文章;作为一个懒人,不能所有博客文章都手动去各家平台发布,只好通过编写脚本来发布。但是除了Github提供了比较详细的Api外,其他国内的博客平台都没有提供对应的接口,但总有办法的。

下面是我对某家博客平台模拟登录流程的记录(打死我都不会说这家平台是S开头的),个人觉得挺有意思的,也能从中学到不少产品安全设计的思路。

工具

  • Babel

  • Cheerio.js

  • SuperAgent

  • Chrome 浏览器

注:工具只是实现结果的一个手段,并不一定需要掌握这些工具,只要知道它们是干嘛的就行了。

开始分析

先进入主页找到用户登录页,如下图所示:

登录

标准的登录框,在这边需要把Chrome的控制台打开,进入Network页,把 Preserve log (页面跳转也能记录日志,感谢 铁臂狗 告知)的选项勾中, 如下图所示:
Chrome 控制台

抓包分析请求,先从输入正确密码开始:

输入正确密码

正确密码的包

我把暴露隐私的两个地方打码了(这两块也是我们接下来要着重要分析的点)

可以从中看到请求头,我们先把这些请求头照抄下来

const base_headers = {
    Accept: '*/*',
    'Accept-Encoding':'gzip, deflate',
    'Accept-Language':'zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,ja;q=0.2',
    'Cache-Control':'no-cache',
    Connection:'keep-alive',
    DNT:1,
    Host:'segmentfault.com',
    Origin: 'http://segmentfault.com',
    Pragma:'no-cache',
    Referer: 'http://segmentfault.com/',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36',
    'X-Requested-With': 'XMLHttpRequest'
    }

排除法删除 Cookie

我们可以看到在请求登录的时候 Header 就已经带有 Cookie 了,这在我平常的设计中没有做过,所以我就试着把 Cookie 删后再请求,看看有什么效果。删除 Cookie 的方法如下所示:

删除Cookie的方法

利用排除法不停删除并继续试着登录,都能完成登录;直到删除 PHPSESSID 的时候发现删除之后再登陆是会报错的,所以这个 PHPSESSID 肯定是有用的(没用过PHP对这个不太了解),因此我断定这个 Cookie 是在后端作为验证登录的一个字段;因此我可以通过在登录之前先下载首页并拿到 Cookie,放到请求头上做作为模拟 Header。

获取 Cookie

import request from 'superagent'

let cookie;

req
.get(urls.mainpage)
.end((err, res) => {
   // 从上图可以看到我们需要的cookie是PHPSESSID开头的
    cookie = res.headers['set-cookie']
                .join(',').match(/(PHPSESSID=.+?);/)[1]
})

获取页面 token

本以为拿到 Cookie 之后就可以开开心心的做登录请求,然而这么简单的话这篇文章页也就没什么写的必要了。

继续分析请求 HTTP 包,可以发现在每次请求的时候,url 后面总是会带一个 queryString(图 2),我在这里耗费了不少时间,毫无头绪,只能追进源码里面摸索。

压缩后的源码

找到上图中的源码,可以看到这个源码是被压缩过的,不要着急,chrome 提供了 formatt 功能,点击最下面的{},可以对压缩的代码重排,至少是勉强可以阅读的代码了。

美化后的代码

接下来的事情就是怎么从这堆代码中抽丝剥茧找到对我们有用的信息,可是这么多的代码一步步看下来也会看到头晕脑胀,眼睛滴血。那么就试试看能不能使用查找的方式从源码中找到我需要的东西。使用快捷键 ctrl+F,键入 /login/login是作为登录的链接的,感觉上可能会有很大概率能搜到相关代码)

搜索代码

很巧的是,搜到了相关的代码。从中可以看到此网站使用了 JQuery 的 Ajax 发送相关 HTTP 请求,那么,url 便是 e.attr("action"),从下面的 DOM 结构能看到 action 是api/user/login

DOM结构

还是没有找到 queryString, 那就换个关键词试试看,这次搜索 _=(看图2,queryString 是由_=拼接起来的)

搜索代码2

从上图可以看到有7个结果,而被黄色标注出来的那行才是我们想要的。JQ 的 ajaxSend 可以在 Ajax 发送之前做一些处理。从上图可以看出,请求的时候在 url 的后面增加一个 n._ ,那就继续去找n._是什么?由于截图截少了,我就不再重新截图,从上图的第一行可以看到 _ 是window.SF.token,由此我们就摸到 token 的 G 点,整个流程明朗了许多。接下来全局搜索 window.SF.token,没找到。我知道 window 是全局变量,为什么把 token 放到 window 上?可以想多的是 token 并没有在当前的 script 标签内。接下来去 index.html 内查找:

token

找到了!可以看到 token 是被包裹在一个独立的 script 标签内,在后端生成HTML模板的时候就已经插入。

找到 token 之后就很简单了,拿到这个字符串表达式,运行,拿到token。
原理我之前写过一篇文章,移步

import cheerio from 'cheerio'
import request from 'superagent'

let cookie;


// 为什么这样做
function getToken(s) {
  let $ = cheerio.load(s)
  , text = $('body script').eq(2).text()
  , fn = new Function('window', text + ';return window.SF.token')
  , token = fn({})

  $ = null
  return token
}


req
.get(urls.mainpage)
.end((err, res) => {
   let token = getToken(res.text)
   
   // 从上图可以看到我们需要的cookie是PHPSESSID开头的
    cookie = res.headers['set-cookie']
                    .join(',')
                    .match(/(PHPSESSID=.+?);/)[1]
})

开始登录吧

拿到 token 和 Cookie ,抓包分析所需要的登录字段:

{
    mail: 'xxxxx@xx.xx', // 邮箱
    password: 'xxxxxxx', // 密码
    remember: '1'  // 是否记住登录
}

登录:

req
.get(urls.mainpage)
.end((err, res) => {
   let token = getToken(res.text)
   
   // 从上图可以看到我们需要的cookie是PHPSESSID开头的
    cookie = res.headers['set-cookie'].join(',')
                .match(/(PHPSESSID=.+?);/)[1]
    
    req
   .post(urls.login)
   .query({'_': token})
   .set(base_headers)
   .set('Cookie', cookie)
   .type('form')
   .send(conf)
   .redirects(0)
   .end((err, res) => {
        console.log(res)
    })
  })
})

总结

世上无难事只怕有心人

登录是最基础也最核心的功能,通过对登录流程的分析,基本弄清楚了此博客平台的验证机制,在分析的过程中斗智斗勇,利用自己掌握的知识一步一步破解谜题的本身就是一件很有意思的事情,以后也可以将此方法用到自己的登录流程设计中。

TODO

登录之后能施展的手段就很多了: 提问题,发表文章,创建标签等等,用到得知识都在上面说过了,按下不表。

有需要源码的同学,欢迎 Star

相关文章:

  • position定位的小问题
  • 分析一下云ERP与本地ERP相比区别在哪里
  • Skia深入分析5——skia文字绘制的实现
  • Linux下Tomcat的安装配置
  • FAT32,NTFS,EXT3,支持的最大分区和单个文件大小?
  • leetcode先刷_Valid Sudoku
  • [译] 怎样写一个基础的编译器
  • 攻城记:Thinkphp框架的项目规划总结和踩坑经验
  • POJ 3421 X-factor Chains(构造)
  • Apache Tez 介绍(译)
  • LinkedList的用法小结
  • 在linux下配置静态IP
  • TotoiseSVN基本用法
  • android studio 无法在可视化页面预览布局文件
  • ubuntu php mysql
  • 【5+】跨webview多页面 触发事件(二)
  • 77. Combinations
  • Android开发 - 掌握ConstraintLayout(四)创建基本约束
  • Android系统模拟器绘制实现概述
  • CentOS学习笔记 - 12. Nginx搭建Centos7.5远程repo
  • GraphQL学习过程应该是这样的
  • HTML5新特性总结
  • Invalidate和postInvalidate的区别
  • Phpstorm怎样批量删除空行?
  • Spring Cloud中负载均衡器概览
  • Vim Clutch | 面向脚踏板编程……
  • Vue官网教程学习过程中值得记录的一些事情
  • Yii源码解读-服务定位器(Service Locator)
  • 短视频宝贝=慢?阿里巴巴工程师这样秒开短视频
  • 构建二叉树进行数值数组的去重及优化
  • 数据可视化之 Sankey 桑基图的实现
  • #{} 和 ${}区别
  • (10)ATF MMU转换表
  • (3)llvm ir转换过程
  • (二)pulsar安装在独立的docker中,python测试
  • (附源码)计算机毕业设计SSM疫情下的学生出入管理系统
  • (六)vue-router+UI组件库
  • (使用vite搭建vue3项目(vite + vue3 + vue router + pinia + element plus))
  • (一) springboot详细介绍
  • (转)树状数组
  • ... 是什么 ?... 有什么用处?
  • ../depcomp: line 571: exec: g++: not found
  • .gitignore文件—git忽略文件
  • .net framework 4.0中如何 输出 form 的name属性。
  • .net Signalr 使用笔记
  • .NET 自定义中间件 判断是否存在 AllowAnonymousAttribute 特性 来判断是否需要身份验证
  • .net6+aspose.words导出word并转pdf
  • .NET构架之我见
  • .net下的富文本编辑器FCKeditor的配置方法
  • [ C++ ] STL---stack与queue
  • [202209]mysql8.0 双主集群搭建 亲测可用
  • [ExtJS5学习笔记]第三十节 sencha extjs 5表格gridpanel分组汇总
  • [hadoop读书笔记] 第十五章 sqoop1.4.6小实验 - 将mysq数据导入HBASE
  • [LaTex]arXiv投稿攻略——jpg/png转pdf
  • [leetcode] Longest Palindromic Substring