郑大一卡通鉴权分析笔记【一】
本文记录一次对郑州大学 豫见郑大 App 进行电费接口分析的过程。
主要记录分析思路和踩坑过程。
自己分析过程中打的笔记有点乱,由 AI 进行了 Markdown 排版
0. 为什么要这么做
今天是去郑州大学报道的第一天,上午报道后,宿管说电费的查询与交费都在 豫见郑大 App 上完成。
我马上就想到,要是我们都没检查电费,万一哪天:
- 打着游戏宿舍电路欠费停电了,电脑帧率瞬间被强制降低
- 快睡着时空调电路欠费停电了,被热起来交电费重新开空调
不愉快的事情还能例举很多……
如果可以定时检查电费,并且在电费不够时提示我,就不会出现这样的顾虑了。
此外,如果可以一直记录电费,长期以来就可以看出电费趋势了!(虽然没什么用)
1. 获取电表数据
最开始的目标非常简单:
获取豫见郑大App里面一卡通平台的电表数据。
我想直接把 App 扔进 MuMu 模拟器(可以 Root)里,然后用 Reqable 抓包分析。
结果第一步就寄了。
1.1 模拟器抓包并不行
把 App 放进 MuMu 模拟器运行,logcat 里报错:
1 | |

这时候第一反应是:
- App 有模拟器检测?【也许,因为有壳】
- 加壳导致的?【确实有壳】
- 或者是不是 ABI 不支持?【多半是,毕竟 Entry 多半只能指 so 文件里的 Entry 了吧】
总之,正常运行并抓包这条路暂时走不通。
于是换一种思路。
1.2 从分享链接入手
在电费页面里发现了一个比较有意思的功能:
分享 → 微信
通过这个功能拿到了分享 URL。
打开这个 URL 并点击“继续访问”,可以进入登录页面。
然而登录后,并没有进入电费页面,服务器 502 了
不过,至少有一个非常重要的收获:
分享页面跳转的目标网址拿到了。
尝试直接访问这个目标网址。
1 | |
结果显示无法获取“树维认证参数”
【顺带一提“树维”是一家科技公司,豫见郑大 App 的包名中 “supwisdom” 就是他们】
看来还是绕不开鉴权。
2. 从 WebView 突破
这时候突然发现:
一卡通页面看起来特别像一个网页!
于是打开了开发者模式的 显示布局边界:
一个大大的输出窗口,四个小蓝角,加一个大叉,基本可以确认这里是 WebView。

既然是 WebView,那就好办了。
尝试使用 Chrome 的 chrome://inspect 连接 Android 设备。
成功连上 WebView!

分析难度瞬间降低了!
在 DevTools 里看网络请求即可!
【题外话:App 首页也是一个 Webview,但是 chrome://inspect 里无法发现这个 Webview,我不知道为什么】
3. 找到电费接口
打开一卡通页面,进入电费查询功能。
发现了目标接口:
1 | |
请求里带上对应的宿舍信息,接口返回个人信息、账户余额、电费余额、计价标准等等
到这里,目标接口已经基本确定。
但是还有一个问题:
鉴权
4. 踩坑:奇怪的 500
首先,我先把整个请求原封不动复制到 Reqable 里,手动发送。
结果服务器返回了 500
???
明明浏览器里可以正常请求,复制出来却 500?
我从请求头请求体一直排查,甚至怀疑有请求重放攻击拦截,搞了我半天.
最后发现一个非常离谱的问题:
如果 JSON 里只要出现空格或者换行,服务器就会直接返回 500。
例如原本:
1 | |
请求时我用了格式化 JSON 功能,大概变成这样:
1 | |
服务器就会 500
man what can i say
什么奇奇怪怪的后端实现!
感觉像是学长们自己写的 JSON 解析(
5. Authorization:有点像 JWT 的鉴权
我尝试逐项删除请求头。
最后发现最重要的鉴权信息是:
1 | |
这种格式第一眼就非常像 JWT 呀!
【很可惜,并不是标准 JWT,解析不出】
请求里还有另外一个:
1 | |
不过把这个请求头删掉之后,接口依然可以正常访问。
所以暂时认为 X-Id-Token 应该不是电费接口当前鉴权所必需的。
我开始找 Authorization 的内容是从哪里来的。
6. 可以续杯的 Token
观察这个 Token 的生命周期。
一个很有意思的现象出现了:
如果不继续访问接口,一小时内这个 Token 就会过期。
如果持续访问接口,例如每 10 分钟访问一次,Token 好像就一直不会过期。
应该是服务器那边刷新了 Token 时效。
理论上可以做一个定时任务,每 10 分钟请求一次接口,就可以保持 Token 一直有效,一直获取电量了。
这样还可以每天自动记录宿舍的用电量曲线,一举两得。
我还沾沾自喜呢,简单。
7. 什么神经的维护时间
我突然就瞟到电费页面上写着 23:40 ~ 01:00 不开放
每天维护一小时干嘛!
当然,合理的解释很多,比如数据库维护、业务系统更新、定时任务等。
但在当时的我看来,多半就是为了防我这种人的。
那我就得看看网页在 Token 过期时干了什么了。
我等过了一小时,再刷新页面。
首先网页尝试访问:
1 | |
返回了 401 Unauthorized。
紧接着就访问了新的接口!
1 | |
随后页面自动刷新。
刷新完成后又可以正常用了。
这说明 Token 过期以后,前端会自动进行 Token 更新。
8. updateToken:Token 还是能续杯
进一步观察 /server/auth/updateToken 接口。
发现这个接口的请求中,会携带旧的 Authorization 里的 Token。
服务器返回了一个新的 accessToken。
然后把这个新的 Token 当作下一次请求的 Authorization 就可以继续正常访问了。
接下来就好办了:
拿到一个 accessToken 放入 Authorization 中请求页面。
如果 accessToken 过期了,就用 /server/auth/updateToken 接口去换新的 accessToken。
已经可以实现定时获取电量了!
9. 双 Token 傍地走,安能辨我是雌雄
9.1 两个 Token 有什么区别
/server/auth/updateToken 接口里返回的不止有 accessToken,还有一个 updateToken。
我看到 localStorage 里也同时存了
1 | |
于是我突发奇想:
如果我把
accessToken删掉,网页会怎么样?
直接删掉 LocalStorage 里的 accessToken。
结果网页很快就发现 accessToken 不见了
然后自动调用 /server/auth/updateToken 接口
而这一次,它传进去的并不是原来的 accessToken,而是:
1 | |
然后成功拿到了新的 Token。
到这里还只能说明 refreshToken 确实可以拿来刷新 Token。
【其实我觉得 refreshToken 就应该被拿来刷新 accessToken】
但接下来发现的东西就有点离谱了。
9.2 两个 Token 似乎没什么区别
继续测试发现:
accessToken 和 refreshToken 居然都可以直接用于鉴权。
而且,如果调用 updateToken 接口,无论传入:
1 | |
还是:
1 | |
都可以正常得到新的:
1 | |
于是目前观察到的行为可以概括成:
1 | |
所以我得出了一个相当离谱的结论:
目前来说,accessToken 和 refreshToken 相当于双保险,两者地位完全相同。
当然,严格来说,这并不能证明后端设计上两者真的完全等价,只能说明:
至少在目前抓到的这些接口和测试场景中,服务器并没有严格区分它们。
9.3 验证我的猜想
既然如此,那就干脆来个更暴力的测试。
原本:
1 | |
交换:
1 | |
然后重新进入网页。
结果:
正常运行。
???
man what can i say
什么奇奇怪怪的后端实现!
至少从目前的实验结果来看,accessToken ≈ refreshToken。