如何使用node模拟登陆Tapd(腾讯敏捷产品研发平台)

由于工作需要,前段时间花了些时间开发了一个sprint会议小工具,工具实现的功能大致有以下几项:

1、根据tapd的taskid自动抓取任务详情

2、根据过滤条件生成的confid批量抓取需求或任务列表。

3、投票功能,在所有人预估工时都完成后展示各自的工时和平均工时,尚有人未完成时要展示那些人已经评估好了时间。

4、参与评估成员的管理(添加/删除/启用/禁用)

5、部分字段的实时编辑功能(比如当前处理人,任务状态,开发工时等)

6、部分字段更新后自动同步到tapd(比如当前处理人,任务状态,开发工时等)

7、参与评估人工时占用情况统计

当然这里并不是要将如何开发这样一个工具,而是介绍这里面最重要的一个步骤,也就是tapd的模拟登录,模拟登录成功后就相当于打通了我们的工具和tapd平台。

那么我们如何模拟登录tapd呢?

首先,按照惯例我们打开Chrome浏览器,打开控制台,切换到network并勾选Preserve Log,准备工作做好了。

接下来,我们打开tapd的登录页(https://www.tapd.cn/cloud_logins/login),然后手动登录一次,这个时候network中记录的请求已经相当多了。

那么哪些是和登录相关的呢?在之前一篇文章《SmartQQ最新登录协议分析》中有提过我们重点关心response中有set-cookie的请求,顺着这个思路我们锁定了两个请求:

1、登录页面加载的时候发起的get请求

这个请求设置了名称为sso-login-token的cookie,做了一个标识这个人已经请求过登录页了,可以进行后续操作,如果直接这个步骤,直接提交数据登录就会失败掉

2、真实登录的请求

这个是一个post请求,而且在验证成功之后设置了登录成功的状态并做了302跳转到后台首页。

我们再看一下登录都提交了一些什么字段

可以看到传输的值并没有多少,但是这些值是怎么来的呢?接下来我们卓哥分析一下

1、data[login][ref], 这个比较好理解,来源页嘛,基本可以写死。

2、data[login][encrypt_key],是一串密文,这个我们就要想想了,也是在smartqq登录协议分析的时候说过,我们可以通过network追踪请求的发起源,进而知道这个值得来源,我们去看一下

结果失望了,触发处写的是other,而不是我们熟悉的js代码堆栈调用过程。那么是不是就没办法了呢?

换个思路,既然请求没有堆栈调用那么确定不是由js触发,就只可能是通过HTML的form标签提交的,结合我们之前看到的登录页和真实post数据的页面URL是一致的,可以判定这个是一个form表单的自提交。接着我们只需要去远吗中找form表单了,

成功搜索到登录表单,也看到了encrypt_key等信息,但是这些input标签并没有初始值,那么值是只可能是在提交之前通过js设置进去的,接着我们使用另外一个技巧,按下快捷键CTRL+SHIFT+F调出全局搜索

然后搜索encrypt_key(因为我们猜测其值是通过js设置的),结果不出所料,搜到了两处,一处是我们源码里看到的form表单,另外一处是在一个js文件里面,这个js文件就是我们想要的了,点进去,发现是压缩的代码,我们使用浏览器的美化工具格式化一下代码

接着在格式化的代码中,我们找到了印证我们猜想的地方,并意外的发现encrypt_iv和password都是在此处设置的,接下来我们的工作就是要解读这段代码了。

我们把这段代码拎出来

$("#login_cn_form").submit(function() {
    if (ssoLogin.currentSSO === !0)
        return !0;
    if (!$("#password_input").attr("data-encrypted")) {
        var t = $("#password_input").val();
        if (commonFunction.checkPasswdIntensity(t),
        t.length > 0) {
            $("#password_input").attr("data-encrypted", !0);
            var o = CryptoJS.MD5(Math.random() + "").toString();
            for (t = CryptoJS.AES.encrypt(t, o, {
                mode: CryptoJS.mode.CBC,
                padding: CryptoJS.pad.ZeroPadding
            }),
            password_encode = t.ciphertext.toString(CryptoJS.enc.Base64); $("#password_input").val() != password_encode; )
                $("#password_input").val(password_encode);
            $("#encrypt_iv").val(t.iv.toString(CryptoJS.enc.Base64)),
            $("#encrypt_key").val(t.key.toString(CryptoJS.enc.Base64))
        }
    }
    var i = validatePart.loginValidator();
    return i
})

这个代码还是有一些干扰,我们只保留我们需要的片段,也就是那三个值的计算。

var t = $("#password_input").val();
var o = CryptoJS.MD5(Math.random() + "").toString();
for (t = CryptoJS.AES.encrypt(t, o, {
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.ZeroPadding
}),
password_encode = t.ciphertext.toString(CryptoJS.enc.Base64); $("#password_input").val() != password_encode; )
    $("#password_input").val(password_encode);
$("#encrypt_iv").val(t.iv.toString(CryptoJS.enc.Base64)),
$("#encrypt_key").val(t.key.toString(CryptoJS.enc.Base64))

仔细看看,也就是那个for里面的内容比较难理解,但其实再细心一点就会发现此处只是一个简单的障眼法,利用for执行了三个语句而已,我们按照正常的逻辑整理一下:

var t = $("#password_input").val();
var o = CryptoJS.MD5(Math.random() + "").toString();
t = CryptoJS.AES.encrypt(t, o, {
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.ZeroPadding
}
password_encode = t.ciphertext.toString(CryptoJS.enc.Base64)
encrypt_iv = t.iv.toString(CryptoJS.enc.Base64)
encrypt_key = t.iv.toString(CryptoJS.key.Base64)

这样看就比较清晰了,也就是利用CryptoJS加密了我们输入的密码,并传递了加密所使用的的key和iv。这三个字段知道后,其余的字段就很简单了,基本都是写死的。

看到这儿,其实登录进去已经只缺实现了,但是需要注意的是在真实登录的时候做了302跳转,我们写模拟登录的时候一定要阻止这次跳转,不然登录成功的cookie设置不了。

下面是我用node写的一个简单的例子:

var CryptoJS = require("crypto-js");
var request = require('request');

const LOGIN = 'https://www.tapd.cn/cloud_logins/login';

const HEADERS = {
  'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'
};

const ACCOUNT = {
  user: 'hanchao@xiatekeji.com',
  pass: '********'
};

request = request.defaults({jar: true});

function getEncodedValue() {
  var o = CryptoJS.MD5(Math.random() + "").toString();
  var t = CryptoJS.AES.encrypt(ACCOUNT.pass, o, {
      mode: CryptoJS.mode.CBC,
      padding: CryptoJS.pad.ZeroPadding
  });
  var password_encode = t.ciphertext.toString(CryptoJS.enc.Base64);
  var iv = t.iv.toString(CryptoJS.enc.Base64);
  var key = t.key.toString(CryptoJS.enc.Base64);
  return {
    iv: iv,
    key: key,
    pass: password_encode
  };
}

function loadLoginPage(next) {
  request({
    url: LOGIN,
    headers: HEADERS
  }, function(err, response, body) {
    if (err) {
      console.log(err);
    } else {
      next && next(body);
    }
  });
}

function doLogin(next) {
  var encodedObj = getEncodedValue();
  var formData = {
    'data[Login][ref]': WORK_TABLE,
    'data[Login][encrypt_key]': encodedObj.key,
    'data[Login][encrypt_iv]': encodedObj.iv,
    'data[Login][site]': 'TAPD',
    'data[Login][via]': 'encrypt_password',
    'data[Login][email]': ACCOUNT.user,
    'data[Login][password]': encodedObj.pass,
    'data[Login][login]': 'login'
  };
  request.post({
    url: LOGIN,
    followRedirect: false,
    formData: formData,
    headers: HEADERS
  }, function(err, response, body) {
    if (err) {
      console.log(err);
    } else {
      next && next(body);
    }
  });
}

function login(next) {
  loadLoginPage(function(preBody) {
    doLogin(function(preBody) {
      next && next(preBody);
    });
  });
}

login(function() {
  console.log('登录成功!');
});

写的比较粗陋,但是基本实现了整个登录,登录之后能做什么不用我说了,但是一定不要做一些可能会影响他人的事情。

最后附上文章开头提到的sprint工具的运行截图:

  • 支付宝二维码 支付宝
  • 微信二维码 微信
相关文章