详细分析小米抢购软件的实现步骤
不知道是饥饿营销还是真的供不应求,小米的火热真的是无法阻挡。众多产品一一亮相,着实吸引眼球,但是一机难求的局面没有改善,让众多米粉败兴而归。我们来实现一个简单的小米抢购软件,让抢购之路多上那么一点点希望。
首先要说明的是小米抢购过程中的很多页面和请求地址都是在开放抢购当天时间点到了之后才开放,抢购结束会关闭,所以你在按照博客的内容自己实现的过程中有请求地址不能访问的,请在抢购开始之后测试,楼主解决不了这个问题。我是在第一次抢购的时候记录请求了哪些地址,做好简单的逻辑之后第二次抢购的时候验证。软件是两个月前完成,当时是可用的。小米抢购的逻辑经常改变,最近这几次没抢,不确定中间请求的地址是否失效。
下面开始分析实现的过程吧。
第一步,模拟登陆。在小米首页点击登陆之后可以看到登陆页面,要求输入邮箱/ID、密码,小米的登陆没有验证码,相对简单了很多。点击登陆按钮可以看到请求的地址是 https://account.xiaomi.com/pass/serviceLoginAuth2,请设置好对应的Request Header,请求参数见下图:
user 和 pwd 是你的用户名和密码,其他的保持不变应该就可以,返回的结果里 desc 是“成功”说明登陆成功,这个时候能拿到一个返回的cookie,这个cookie不是我们最后需要的cookie,还需要一步请求之后拿到的cookie才能用,从登陆页面请求的抓包结果来看确实还有一步。从上一步的返回的结果里获得notificationUrl的值,形如:/auth/service?userId=xxxxxxxx&_sign=xxxxxxxx&nonce=xxxxxxxx,完整地址为 https://account.xiaomi.com/auth/service?userId=xxxxxxxx&_sign=xxxxxxxx&nonce=xxxxxxxx,带着上一步拿到的cookie访问这个页面,拿到返回的cookie保存起来,这个cookie就是可用的,模拟登陆到这里就完成了。
第二步,获得手机版本选择页面的地址。请求地址形如:http://tp.hd.mi.com/getpath/cn?m=1&jsonpcallback=getpath&_=1416887144988,最后一个参数为当前时间的时间戳,这个地址只有到开放购买的时候才有效,平时访问是不通的。从返回的结果中拿到path对应的值,形如:http://s1.mi.com/open/3612A6B4D011142E4D533DE85CCBA5D1/choosePhone.html?_20141125,这个页面是我们在抢购的时候看到的手机版本选择的页面,获取页面内容,里面有很多有用的信息。
先看一下这个页面里的部分js内容。
1 init: function() { 2 this.inTheQueue = !1, 3 this.phoneSku = "", 4 this.phoneType = "", 5 this.hdinfoData = null, 6 this.startTime = new Date("2014/12/9 12:00:00").getTime() / 1e3, 7 this.nextDate = "12月16日", 8 this.showMod = !0, 9 this.modType = null, 10 this.fkNum = 0, 11 this.isReg = "true", 12 this.hdget_date_tmp = "{{M}}md{{Y}}y47d15s", 13 this.cookies = { 14 isStart: "XM_Hd_Start", 15 buySucc: "XM_Buy_Succ", 16 userId: "userId", 17 login: "xm_order_btauth" 18 }, 19 this.home = "http://s1.mi.com/open/index.html", 20 this.hdgetUrl = "http://tp.hd.mi.com/hdget/cn?product={{SKU}}&addcart=1&m=1&fk={{FK}}&uagent={{TODAY}}", 21 this.hdinfoUrl = "http://tp.hd.mi.com/hdinfo/cn", 22 this.timestampUrl = "http://tp.hd.mi.com/gettimestamp", 23 this.getmodeUrl = "http://tp.hd.mi.com/getmode/cn/?product=", 24 this.nextBookUrl = "http://a.hd.mi.com/productv2/book/a/18#MIPHONE", 25 this.ordeSite = "http://order.mi.com", 26 this.shopCartUrl = this.ordeSite + "/cart/add/{{SKU}}?source=bigtap&token={{TOKEN}}", 27 this.addCartNext = this.ordeSite + "/event/success?goodsid={{SKU}}", 28 this.loginUrl = "http://s1.mi.com/zt/xm_account/limitfacade.html?third=http%253A%252F%252Forder.mi.com%252Flogin%252Fcallback%253Ffollowup%253Dhttp%25253A%25252F%25252Fs1.mi.com%25252Fopen%25252Findex.html%2526sign%253DNjEzYmU3ZTJkOWRlY2FiZDQ5NDEwNzEyZjNiMjg0NDA0MGYxYWY3Mg%252C%252C%26sid%3Dmi_eshop&sid=mi_eshop&callback=http%253A%252F%252Forder.mi.com%252Flogin%252Fcallback%253Ffollowup%253Dhttp%25253A%25252F%25252Fs1.mi.com%25252Fopen%25252Findex.html%2526sign%253DNjEzYmU3ZTJkOWRlY2FiZDQ5NDEwNzEyZjNiMjg0NDA0MGYxYWY3Mg%252C%252C&sign=dK3nqW%252FKhFM3Tl7Jyt9%252FGt3jOI8%253D", 29 this.noPresaleGoods = [], 30 this.noBookGoods = [], 31 this.isHm = ["2143300001", "2143400005", "2143200006", "2141600007", "2140700031"]; 32 var a = this; 33 return xmCookie(a.cookies.userId) && xmCookie(a.cookies.login) || (location.href = a.loginUrl), 34 xmCookie(a.cookies.isStart) ? a.getHdInfo() : a.checkTime(), 35 $("[data-close-target]").on("click", 36 function() { 37 var b = $(this).attr("data-close-target"); 38 return a.hideBox(b), 39 !1 40 }), 41 $("#submitBtn").on("click", 42 function() { 43 return ! a.phoneSku || $(this).hasClass("btn-disabled") ? (alert("请选择您要购买的手机"), !1) : $.inArray(a.phoneSku, a.isHm) >= 0 ? void(location.href = "http://order.mi.com/event/selectPacket/goodsid/" + a.phoneSku) : "true" !== a.isReg ? (a.showBox("Tip", 44 function() { 45 a.getTipMsg("reg") 46 }), !1) : void(a.showMod ? (a.getmode(), a.showBox("Fk")) : (a.startQueue(), a.getDmSys())) 47 }), 48 $("#boxCacheBtn").on("click", 49 function() { 50 a.hideBox("all"), 51 $("#submitBtn").trigger("click") 52 }), 53 "undefined" != typeof HDOVER && HDOVER === !0 ? (location.href = a.home, !1) : ($(".J_nextDate").html(a.nextDate), $(".J_bookBtn").attr("href", a.nextBookUrl), $(".J_fkLoading").on("click", ".J_reloadFk", 54 function() { 55 $(this).parent().html('<img src="http://img03.mifile.cn/webfile/images/2014/cn/loading.gif">'), 56 a.getmode() 57 }), void $("#fkNum").on("keyup", 58 function() { 59 $(this).val().length; 60 $("#boxFkBtn").removeClass("btn-disabled").off().on("click", 61 function() { 62 a.checkFk() 63 }) 64 })) 65 }
仔细分析一下这段js,发现抢购后续的请求地址和参数格式在这里都能找到,hdgetUrl,hdinfoUrl,getmodeUrl,shopCartUrl 这几个后面都会用到,抓包分析后面几步的过程可以和这里的地址验证,先保存下来。下面要做的工作是计算js中{{TODAY}}对应的值,将hdgetUrl中的值替换,计算逻辑可用下面的,
Pattern dateTmpPattern=Pattern.compile("hdget_date_tmp=\"(.*?)\"");
Matcher dateTmpMatcher=dateTmpPattern.matcher(result);
if(dateTmpMatcher.find()) {
dateTmp=dateTmpMatcher.group(1);
dateTmp=dateTmp.replace("{{Y}}",YEAR+"").replace("{{M}}",MONTH).replace("{{D}}",DAY);
hdGetUrl=hdGetUrl.replace("{{TODAY}}",dateTmp);
}
YEAR、MONTH、DAY 为当前日期。
第三步,获得产品状态信息。这里用到了 hdinfoUrl,请求地址为http://tp.hd.mi.com/hdinfo/cn?jsonpcallback=hdinfo&_=1416888070538(下面不说明的时候比较长的数字应该就是指当前的时间戳)。请求返回的数据格式如下:
{"stime":1416979023,"pmstart":false,"status":{"2141700014":{"hdstart":false,"hdstop":true,"reg":true},"2143000004":{"hdstart":false,"hdstop":true,"reg":true},"2143400001":{"hdstart":false,"hdstop":true,"reg":true},"2143400004":{"hdstart":false,"hdstop":true,"reg":true},"2143600001":{"hdstart":false,"hdstop":true,"reg":true},"2144000012":{"hdstart":false,"hdstop":true,"reg":true},"2144100013":{"hdstart":false,"hdstop":true,"reg":true},"2144100014":{"hdstart":false,"hdstop":true,"reg":true}},"dbe5a2":false}
包含了每种产品的可售状态,可以根据这个信息判断是否能购买,在这里可以过滤掉不能买的信息。
产品代号和名称的对应关系我做了一个对应,这个信息比较老了,请根据每次购买的情况更新:
2143000004----小米4联通3G版 亮白16GB 1999
2143400001----小米4联通3G版 亮白64GB 2499
2143100007----小米4联通4G版 亮白16GB 1999
2143400004----小米4电信3G版 亮白16GB 1999
2144000012----小米4移动4G版 雅黑16GB 1999
2144100014----小米4移动4G版 雅黑64GB 2499
2144800007----小米4移动4G版 黑色(金属原色框)16GB 1999
2143600001----小米4移动4G版 亮白16GB 1999
2144100013----小米4移动4G版 亮白64GB 2499
2143200006----红米Note 移动4G增强版 899
2143400005----红米Note 联通4G增强版 899
2141600007----红米1S联通3G版 金属灰 799
2140700031----红米1S电信3G版 金属灰 799
2143300001----红米1S移动4G版 金属灰 599
2141700014----小米路由器 mini 129
2143000001----小米手环 79
要购买哪种产品只需要产品代号信息(应该是sku)就够了。
第四步,获取验证码或验证问题。用到了getmodeUrl地址,请求路径为 http://tp.hd.mi.com/getmode/cn/?product=xxxxxxxxxx&jsonpcallback=getmode&_=1416888070539,将product值换成你要去买的产品的sku值。根据返回response头的Content-Type值判断是验证码图案还是验证问题,image/jpeg 类型的返回信息应该是验证码,text/html 应该是验证问题。根据不同类型,输入答案后进入第五步。
第五步,验证答案。地址和第四步中是类似的,多了一个答案参数,http://tp.hd.mi.com/getmode/cn/?product=2144000012&vecode=89&jsonpcallback=getmode&_=1416888070540,返回结果中code值是“1”说明验证成功,可以进行下一步,否则重新输入验证答案。
第六步,最后获得产品信息,获得下一步TOKEN对应的hdurl。这一步用到了hdgetUrl,http://tp.hd.mi.com/hdget/cn?product={{SKU}}&addcart=1&m=1&fk={{FK}}&uagent={{TODAY}},SKU为要购买产品的sku,FK为第四步出现验证码或问题的答案,TODAY已在前面替换成一个和日期有关动态的参数,拿到请求这个地址的返回结果,
hdcontrol({"d22a51":10,"login":true,"pmstart":false,"status":{"2144800007":{"hdstart":false,"hdstop":true,"hdurl":""}}})
其中hdurl有值的时候说明这一步是成功的,拿到这个hdurl进入第七步,如果你得到的结果和我一样没有hdurl的值,那么不好意思,你没法加入购物车,后面会提示出错的,这一步可以多试几次,看看能不能运气好就能买了。
第七步,加入到购物车。用到了shopCartUrl ,地址 http://order.mi.com/cart/add/2144000012?source=bigtap&token={{TOKEN}}&jsonpcallback=getdata,将这里的 TOKEN 替换成第六步获得的 hdurl , 根据这个请求的返回结果判断是否添加成功,
下面是返回数据的格式,
{"code":-1,"message":"\u6dfb\u52a0\u8d2d\u7269\u8f66\u9700\u8981\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55\uff01","msg":"\u6dfb\u52a0\u8d2d\u7269\u8f66\u9700\u8981\u767b\u5f55\uff0c\u8bf7\u5148\u767b\u5f55\uff01"}
可以根据code的值判断是否成功,后面的为提示信息。
抢购过程中的关键点都分析完了,再次强调一下,抢购的逻辑经常改变,不保证这个过程还适应现在的逻辑,需要自己在开放购买的时候实测。
上一张截图