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中同样的效果,可以做下面的几步:
- 让H5页面识别浏览器为Keep站内。
要识别站内就是使用上面的
userAgent
,打开开发者工具,然后选择Network conditions
面板,去掉Select automatically
的勾选,然后把上面的userAgent
粘贴到下面的输入框中,如下,然后刷新一下userAgent
就生效了。
- 获取cookie。
Keep接口认证是基于
JWT
来实现的。我们使用Charles
来查看任一接口的cookie
,会发现有一个authorization
的字段,这个就是JWT
的关键,如下:
- 设置cookie。
为了页面能正常运行,我们把所有的
cookie
信息,都设置进去。打开浏览器的开发者工具,然后依次把cookie
粘贴进去,如下:
此时刷新页面,可以看到页面已经可以正常运行了,如下:
客户端事件
客户端事件是H5和客户端(这里只有移动端)交互的指令,其实就是一个特定协议的字符串,前端使用location.href = 客户端事件字符串
来执行客户端事件,在Keep中为了方便调试,也可以扫码来执行这些事件。举个例子,打开webview的事件是keep://webview/
后面跟着encode的URI就可以实现跳转页面了,比如要使用Keep来跳转本博客,就可以如下:keep://webview/https%3a%2f%2fwww.kai666666.top%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
的数据库。
建表脚本如下:
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一下。
脚本大致如下:
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
中获取方式如下:
最后你就可以通过SQL语句查询数据了:
# 全马
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个半马了?。
- 编程语言之间的百舸争流
- Mysql连接错误:Lost connection to Mysql server at 'waiting for initial communication packet'
- 适应现代变化的数据架构
- Linux下修改系统编码的操作记录
- 微信公众平
- linq to xml复习
- web cache server方案比较:varnish、squid、nginx
- Nginx虚拟目录alias和root目录
- Nginx的https配置记录以及http强制跳转到https的方法梳理
- VPC下访问FTP的问题
- 分析车辆雨刮频次计算降雨量 大数据服务天气预报
- 干货:这里有一份小程序接入微信支付避雷指南
- Mysql占用过高CPU时的优化手段
- 服务器端加入自动运行的JS代码
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 使用ES5,ES6和SAP ABAP实现非波拉契数列Fibonacci
- SpringBoot整合自定义注解
- nginx被动检测
- Python获取股票历史数据
- 实习第四周
- 浅谈推进有赞全站 HTTPS 项目-工程篇
- Checks autowiring problems in a bean class问题解决
- 有赞权限系统
- python处理txt文件常用方法
- 修改springboot运行时命令行Banner
- 适应性页面自己的看法
- Vant 1.0 正式发布:轻量、可靠的移动端 Vue 组件库
- SAP CDS view单元测试框架Test Double介绍
- 漫谈 React 组件库开发(二):组件库最佳实践
- 搭建简易的物联网服务端和客户端-移动家庭能力平台【1】(二十三)