Keep APP技术研究

最近在研究运动软件Keep,就是那个自律给我自由的Keep。主要方法是使用Charles来抓包,然后查看接口。由于Charles是一款Mac的应用,所以Windows系统,可能不能实践了。另外安卓手机限制不能抓包HTTPS的协议,所以也不能实践了。

现在分享一下我的研究成果,本文可能触及到Keep软件的一些特殊操作,大家谨慎使用,本文仅供学习和交流使用,如果侵犯到Keep的相关利益,请联系我。


Keep是典型的混合式开发,也就是前端H5 + 后端 + 移动端(安卓和iOS),大多界面都是使用了前端技术开发的,主要前端框架是基于VUE来做的。

主要域名

后端服务域名:https://api.gotokeep.com
主站H5:https://show.gotokeep.com
活动等H5:https://m.gotokeep.com
静态资源CDN:https://static1.keepcdn.com
监控:https://apm.gotokeep.com
智能设备:https://kit.gotokeep.com

userAgent

userAgent是混合开发中,H5用来识别APP内部与外部的重要依据。前端可以通过JavaScript代码window.navigator.userAgent来获取,Keep的userAgent`如下:

Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148;Keep/6.43.0 (iPhone; iOS 13.5.1; Scale/2.00);Keep/6.43.0 (iPhone; iOS 13.5.1; Scale/2.00)

当然不同系统是不一样的,其中最重要的是最后Keep/版本号(其他信息)这一段,至于为什么要写2遍,我也不清楚,难道客户端植入的时候多写了一遍?

调试页面

Keep线上的页面都是线上环境,调试线上环境的其实也没有多大的意义。由于我们拿不到Keep的源代码,所以只能通过线上代码简单地看看Keep页面的结构。

Keep使用了vue + vue-router + vuex这样的框架组合,它的页面链接的最后一级是用户的userId,我们以“我的等级”页面为例,如:https://show.gotokeep.com/experience/grades/xxxxxxxxxxxxxxxxxxxxxxxx?kg=16其中xxxxxxxxxxxxxxxxxxxxxxxx就是userId,由于用户的userId是隐私数据,所以我就那x来代替了(下面所有有userId的地方,我都会用xxxxxxxxxxxxxxxxxxxxxxxx来代替)。

浏览器直接打开这个页面,发现报错了,仔细一看,你会发现是接口https://api.gotokeep.com/diamond/v1/users/xxxxxxxxxxxxxxxxxxxxxxxx/privilegeWall/levels返回了401,接口401说明没有授权。为了让页面实现在Keep中同样的效果,可以做下面的几步:

  1. 让H5页面识别浏览器为Keep站内。

    要识别站内就是使用上面的userAgent,打开开发者工具,然后选择Network conditions面板,去掉Select automatically的勾选,然后把上面的userAgent粘贴到下面的输入框中,如下,然后刷新一下userAgent就生效了。

    修改userAgent

  2. 获取cookie。

    Keep接口认证是基于JWT来实现的。我们使用Charles来查看任一接口的cookie,会发现有一个authorization的字段,这个就是JWT的关键,如下:

    查看cookie

  3. 设置cookie。

    为了页面能正常运行,我们把所有的cookie信息,都设置进去。打开浏览器的开发者工具,然后依次把cookie粘贴进去,如下:

    设置cookie

此时刷新页面,可以看到页面已经可以正常运行了,如下:

运行的页面

客户端事件

客户端事件是H5和客户端(这里只有移动端)交互的指令,其实就是一个特定协议的字符串,前端使用location.href = 客户端事件字符串来执行客户端事件,在Keep中为了方便调试,也可以扫码来执行这些事件。举个例子,打开webview的事件是keep://webview/后面跟着encode的URI就可以实现跳转页面了,比如要使用Keep来跳转本博客,就可以如下:keep://webview/https%3a%2f%2fwww.kai666666.com%2f,你可以使用这个工具来encode,把刚才的事件,转换为二维码,如下:

二维码

现在打开Keep扫一扫,上面的二维码,你就可以用Keep进入本博客网站了。要把字符串转换为二维码可以使用草料二维码

https://api.gotokeep.com/config/v2/basic?refresh=true接口中定义了更多的事件:

描述 事件
精选 keep://discover_web
训练 keep://discover_course
饮食 keep://discover_food
商城 keep://discover_store
精选 keep://discovery/explore
训练 keep://discovery/course
攻略 keep://discovery/guide
饮食 keep://discovery/diet
商城 keep://discovery/product
推荐 keep://hottabs/hot
热门视频 keep://hottabs/video
运动时刻 keep://hottabs/story
training_训练课程 keep://discover_course/
activity_热门活动 keep://hot_activities
hashtag_话题讨论 keep://hashtags_index
group_小组推荐 keep://groups_index/
kol_达人推荐 keep://recommend_keepers/
article_精选文章 keep://selections
热门 keep://timeline/hot
关注 keep://timeline/follow
逛逛 keep://timeline/wander
训练 keep://homepage/content?tabId=ZnVsbENvbnRlbnQ=
跑步 keep://homepage/running?tabId=cnVubmluZw==
瑜伽 keep://homepage/yoga?tabId=eW9nYQ==
行走 keep://homepage/hiking?tabId=aGlraW5n
骑行 keep://homepage/cycling?tabId=Y3ljbGluZw==
Kit keep://homepage/keloton?tabId=a2Vsb3Rvbg==
数据中心 keep://datacenter?type=all&period=day
跑步历史记录 keep://datacenter?type=running&period=day
每周目标 keep://weeklypurpose
身体档案 keep://bodydata
运动能力 keep://physical_test_list
运动概况 keep://physical_summary
运动日记 https://show.gotokeep.com/usersfulldiary
步数记录 keep://steps_dashboard
连接应用和设备 keep://oauth/list
我的收藏 keep://my_favorites
我的活动 keep://activities
训练营历史 keep://bootcamp/history
我的路线 keep://my_running_routes
我的运动小队 https://show.gotokeep.com/outdoor/groups/list
我的最佳成绩 keep://running/best_records
我的 Class keep://classes/mine
购物车 keep://shopping_cart
我的钱包 https://show.gotokeep.com/wallet
优惠券 keep://store_coupons
购买记录 keep://purchase_history
Keepland 课程 https://keepland.gotokeep.com/my_course?pulldownrefresh=true

从上面可以看到很多https://show.gotokeep.com开头的地址用Keep扫码也是可以直接进入的,但是我们自己的https网站却不能,可见Keep对自己的白名单内的域名做了特殊处理(相当于其他页面使用了keep://webview/事件)。

小应用

Keep并没有提供一种查看自己跑了多少个全马,或者跑了多少个半马这样的功能。现在我们写个脚本把自己的跑步数据存入我们自己的数据库中,并通过SQL查询出我们跑了多少个半马。

这里假设你已经安装了MySQL,并且已经建立了一个名叫keep_running的数据库。

建表脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
DROP TABLE IF EXISTS running_data;

CREATE TABLE IF NOT EXISTS running_data (
id VARCHAR(100) NULL,
statsType VARCHAR(100) NULL,
trainingCourseType VARCHAR(100) NULL,
subtype VARCHAR(100) NULL,
statsName VARCHAR(100) NULL,
doneDate VARCHAR(100) NULL,
icon VARCHAR(100) NULL,
statsSchema VARCHAR(100) NULL,
workoutFinishTimes INT NULL,
duration INT NULL,
distance INT NULL,
steps INT NULL,
kmDistance DOUBLE NULL,
calorie INT NULL,
averagePace DOUBLE NULL,
averageSpeed DOUBLE NULL,
exerciseInfo VARCHAR(500) NULL,
statsStatus INT NULL,
trackWaterMark VARCHAR(100) NULL,
workoutId VARCHAR(100) NULL,
vendorSource VARCHAR(100) NULL,
vendorManufacturer VARCHAR(100) NULL,
vendorGenre VARCHAR(100) NULL,
vendorDeviceModel VARCHAR(100) NULL,
vendorRecordId VARCHAR(100) NULL,
heartRates TEXT NULL,
averageHeartRate DOUBLE NULL,
maxHeartRate VARCHAR(100) NULL,
isDoubtful VARCHAR(100) NULL,
logsType VARCHAR(100) NULL,
statsDate VARCHAR(100) NULL,
calorieSum INT NULL,
durationSum INT NULL
);

要得到自己的跑步数据,可以调用https://api.gotokeep.com/pd/v3/stats/detail接口来获取,由于浏览器端会有跨域的问题,所以我们就直接跑一个node脚本就行了。

这里需要用到额外的两个库,一个是axios,用来发送http请求的;另一个库就是mysql,用来把数据存到数据库的。大家自己运行npm install一下。

脚本大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
const axios = require('axios').default
const mysql = require('mysql');
const connection = mysql.createConnection({
host : "localhost",
user : "root",
password : "自己的数据库密码",
database : "keep_running"
});

connection.connect();

queryAndInsert(connection);


let globalIndex = 1;

function queryAndInsert(connection,lastDate){
axios.get('https://api.gotokeep.com/pd/v3/stats/detail', {
params: { dateUnit: 'all',type:'running',lastDate },
withCredentials: true,
headers:{
// TODO:这里是headers的数据,需要使用你自己的 不然查询会失败的
}
}).then(res=>{
let sql =
`INSERT INTO running_data (
id, statsType, trainingCourseType, subtype, statsName,
doneDate, icon, statsSchema, workoutFinishTimes, duration,
distance, steps, kmDistance, calorie, averagePace,
averageSpeed, exerciseInfo, statsStatus, trackWaterMark, workoutId,
vendorSource, vendorManufacturer, vendorGenre, vendorDeviceModel, vendorRecordId,
heartRates, averageHeartRate, maxHeartRate, isDoubtful, logsType,
statsDate, calorieSum, durationSum)
VALUES(?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?);`
let data = res.data.data
let lastTimestamp = data.lastTimestamp
if (lastTimestamp === 0 || !data.records || data.records.length === 0) {
connection.end();
} else {
let posts = []
data.records.forEach(record=>{
(record.logs||[]).forEach(log=>{
let stats = log.stats
if (stats) {
stats.vendor = stats.vendor || {}
stats.heartRate = stats.heartRate || {}
posts.push([
stats.id, stats.type, stats.trainingCourseType, stats.subtype, stats.name,
stats.doneDate, stats.icon, stats.schema, stats.workoutFinishTimes, stats.duration,
stats.distance, stats.steps, stats.kmDistance, stats.calorie, stats.averagePace,
stats.averageSpeed, stats.exerciseInfo, stats.status, stats.trackWaterMark, stats.workoutId,
stats.vendor.source, stats.vendor.manufacturer, stats.vendor.genre, stats.vendor.deviceModel, stats.vendor.vendorRecordId,
stats.heartRate.heartRates, stats.heartRate.averageHeartRate, stats.heartRate.maxHeartRate, stats.isDoubtful, log.type,
record.date, record.calorieSum, record.durationSum
])
}
})
})

posts.forEach(post=>{
connection.query(sql, post, function (error, results, fields) {
if (error) throw error;
console.log('成功插入了一条数据:' + globalIndex);
globalIndex++;
});
})

queryAndInsert(connection,lastTimestamp)
}
})
}

注意上面headers处,需要换成自己数据的键值对形式,在Charles中获取方式如下:

获取headers数据

最后你就可以通过SQL语句查询数据了:

1
2
3
4
5
6
7
8
9
10
11
# 全马
SELECT * FROM running_data WHERE distance > 42195 ORDER BY distance DESC;

# 半马
SELECT * FROM running_data WHERE distance >= 21097.5 and distance < 42195 ORDER BY distance DESC;

# 10公里
SELECT * FROM running_data WHERE distance >= 10000 and distance < 21097.5 ORDER BY distance DESC;

# 5公里
SELECT * FROM running_data WHERE distance >= 5000 and distance < 10000 ORDER BY distance DESC;

通过数据查询可以看出我自己跑了10个半马了😀。

我的成绩

-------------本文结束 感谢您的阅读-------------
0%