为什么80%的码农都做不了架构师?>>>
声明:本文所有的内容只作学习使用。
菜鸟裹裹是阿里巴巴旗下菜鸟网络的产品,可用于查询淘宝的快递单号,除了快递状态、进程,还包含了淘宝发货的流程。最近因一个产品需求,需要同步淘宝的物流信息,故研究下如果调用接口来自动获取。
研究思路
- 菜鸟裹裹官网,输入快递单号查询,示例:491772628733,查询前打开浏览器调试窗口
- 观察NetWork窗口,过滤XHR,发现发送了两个请求,观察response,第一个ret为:"FAIL_SYS_ILLEGAL_ACCESS::非法请求",第二个调用成功
- 对比两个请求的request参数,只是少了一个c值,细心的话会发现第一个的response中返回结果也包含了c值
- 查询接口包含12个参数,为了调用需要弄清楚各个参数的含义以及生成方式
代码跟进
dom元素及事件
网页的按钮触发就意味着按钮点击事件触发,查询按钮的dom为
其中id为J_SearchBtn,查看元素的Event Listeners,根据当中的click事件可知注册的源代码位置
$("#J_SearchBtn").on("click", function() {
if (!$(".search-container").hasClass("loading")) {
var o = $.trim($("#J_SearchInput").val());
if ("" === o)
return;
e._handleSearch(o)
}
})
调用链路
继续跟进代码调用,可发现调用链路为
- _handleSearch
- _requestPackage
- lib.mtop.request
- n.prototype.request
- c.__processRequestMethod, c.__processToken, c.__processRequestUrl, c.__processRequest
- __getTokenFromCookie, __requestJSON
代码在主要在https://g.alicdn.com/mtb/lib-mtop/2.4.2/mtop.js文件中
// 函数_requestPackage,从这可以得到api、AntiCreep、v、data、timeout、dataType这6个参数
lib.mtop.request({
api: i.queryLogisticPackageByMailNo,
AntiCreep: !0,
v: "1.0",
data: {
mailNo: e
},
timeout: 5e3,
type: "GET",
dataType: "json",
isSec: 0,
ecode: 0
}, o, r)
// 函数__processRequestUrl,从这可以得到jsv、appKey、t、sign这4个参数
var f = "//" + (d.prefix ? d.prefix + "." : "") + (d.subDomain ? d.subDomain + "." : "") + d.mainDomain + "/h5/" + c.api.toLowerCase() + "/" + c.v.toLowerCase() + "/"
, g = c.appKey || ("waptest" === d.subDomain ? "4272" : "12574478")
, i = (new Date).getTime()
, j = h(d.token + "&" + i + "&" + g + "&" + c.data)
, k = {
jsv: x,
appKey: g,
t: i,
sign: j
}
// 函数__processRequestMethod,从这可以得到参数type
var b = this.params
, c = this.options;
"get" === b.type && "jsonp" === b.dataType ? c.getJSONP = !0 : "get" === b.type && "originaljsonp" === b.dataType ? c.getOriginalJSONP = !0 : "get" === b.type && "json" === b.dataType ? c.getJSON = !0 : "post" === b.type && (c.postJSON = !0),
a()
// 函数n.prototype.__requestJSON,可以得到参数c,实际是cookie中读取_m_h5_c值
h.CDR && j(y) && (h.querystring.c = decodeURIComponent(j(y))),
// 函数__getTokenFromCookie,其中y为_m_h5_c,逻辑就是从cookie中读取_m_h5_c值,字符串分别使用';'和'_'拆分两次,得到第一段
var a = this.options;
return a.CDR && j(y) ? a.token = j(y).split(";")[0] : a.token = a.token || j(z),
a.token && (a.token = a.token.split("_")[0]),
o.resolve()
cookie中_m_h5_c值怎么设置的,cookie的设置有两种方式 1. http请求的的response通过Set-Cookie来设置 2. 通过JS的document.cookie来设置 通过观察所有的http请求,当中并没有该cookie值设置,所以可以确定是通过JS设置的
关键参数
参数中的sign值是经过加密算法得到的,因为已经经过压缩较难看出是什么算法,只能猜测尝试,长度32位,猜想md5算法
常用加密算法加密结果分析:
|算法|加密结果| | ------ | ------ | |MD5|32位| |SHA1|40位| |SHA224|56位| |SHA256|64位| |AES,DES|结尾为=|
// 加密前
6e16c205e17e519707aae3e0938bde68&1534671229873&12574478&{"mailNo":"491772628733"}
// 加密后
7b3a2b38d314ae5d619ceaf0ab55eeab
浏览器模拟
至此接口调用所需的12个参数值来源及规则就都知道了,按照规则拼接好接口调用,却发现依旧返回错误
[ 'FAIL_SYS_TOKEN_EMPTY::令牌为空' ]
既然是模拟浏览器行为,服务器端可能性会校验Referer、Origin等值,查看浏览器的请求中,也确实包含,所以将http请求的Header修改,设置上这两个值。经验证只需要添加Origin即可。
其他猜想
- cookie值中的第三段与当前时间相差2小时30分钟,应该是该cookie在服务器的失效时间
- 服务器端针对cookie、sign、t、appKey四个参数,会再进行加密校验,并通过时间来确定是否已经过期
完整demo代码-nodejs版本
const axios = require('axios')
const crypto = require('crypto')
function getSign(token, now, appKey, data) {
const md5 = crypto.createHash('md5')
const signStr = `${token}&${now}&${appKey}&${data}`
md5.update(signStr)
return md5.digest('hex')
}
// cookie默认值,随便填一个即可
async function queryLogistic(mailNo, cookie = '078c9a3abffad4fc5c0dba2509eb19a0_1534611715157;5ffe99dead01566b71a5107d653cfccb') {
const now = Date.now()
const appKey = '12574478'
const mailData = JSON.stringify({ mailNo })
const token = cookie.split('_')[0]
const sign = getSign(token, now, appKey, mailData)
const { data } = await axios({
url: 'https://h5api.m.taobao.com/h5/mtop.cnwireless.cnlogisticdetailservice.wapquerylogisticpackagebymailno/1.0/',
headers: {
Origin: 'https://www.guoguo-app.com'
},
params: {
jsv: '2.4.2',
appKey,
t: now,
sign,
api: 'mtop.cnwireless.CNLogisticDetailService.wapqueryLogisticPackageByMailNo',
AntiCreep: true,
v: 1.0,
timeout: 5000,
type: 'originaljson',
dataType: 'json',
c: cookie,
data: mailData,
}
})
return data
}
(async() => {
const {c, data, ret} = await queryLogistic('491772628733')
// 如果返回c值,则需要重新使用返回的cookie值发送请求
if (c) {
const resp = await queryLogistic('491772628733', c)
console.info(resp)
} else {
console.info(data)
}
})()