采集得物首页信息
采集得物首页信息
绕过更新
先通过断网绕过强制更新

抓包分析
因为该apk禁止使用代理抓包,我们这里使用SocksDroid抓包
先关闭手机代理,再配置SocksDroid的IP和端口,这里实践过程中打开SocksDroid就无法正确联网,我们还是直接使用Reqable

因为不是post请求,所以没有请求体。我们要主要找的是文本GET请求。


我们发现请求头中的X-Auth-Token内容不能随意删除
反编译破解
newSign

查找观察发现可疑的几个参数在都在同一个类中

我们观察发现该代码是在Intercept下完成,而在 Android 正向开发中,Intercept(拦截) 是一种核心设计模式,用于在关键流程中插入自定义逻辑,实现非侵入式的功能增强。
okhttp的Interceptor拦截器源码解析 - 简书
21.安卓逆向2-frida hook技术-HookOkHttp的拦截器-CSDN博客
了解Intercept基础后就可以看出,该代码是一个 OkHttp 网络拦截器,它的主要目的是在网络请求发出之前,动态地修改请求,特别是添加或替换签名参数(newSign)。
OkHttp源码解析-CSDN博客
深入浅出 OkHttp 源码解析及应用实践 - vivo互联网技术 - 博客园
在拦截器中,那么所有请求都会带有这个newSign。观察代码猜测走RequestUtils.c 。

c代码分析
我们进行hook定位测试一下。
1 | |

我们可以发现hook到的值与抓包得到的相同,可以确定位置正确,我们接着分析。
RequestUtils.c is called: map=[object Object], j2=1762780383093 参数字典查看类型: Json.stringify(map)="<instance: java.util.Map, $className: java.util.HashMap>", j2=1762780383093 RequestUtils.c mapStr={"abValue":"1","deliveryProjectId":"0","abRectagFengge":"0","abType":"social_brand_strategy_v454","limit":"20","lastId":"","abRecReason":"0","abVideoCover":"2"} RequestUtils.c result=448c8c4512de202320745beaddd5fe4f

结合代码对比可以发现就是将这些参数加密后得到的newSign。那么我们就进一步分析一下该代码。
1 | |
代码详解
1. 热修复防护层(Robust 框架)
PatchProxyResult proxy = PatchProxy.proxy(..., changeQuickRedirect, true, 6612, ...); if (proxy.isSupported) { return (String) proxy.result; }- 作用:线上热更新拦截,若存在补丁则直接返回补丁结果
- 逆向影响:Hook 时必须绕过,否则会执行混淆后的补丁逻辑
- 绕过方法:强制返回
isSupported = false
2. 线程安全与空值保护
public static synchronized String c(...) { synchronized (RequestUtils.class) { ... } } if (map == null) { return ""; }- 双保险同步:静态方法锁 + 类对象锁,防止并发签名冲突
- 防御性编程:空 map 直接返回空字符串,避免 NPE
3. 参数注入(签名盐值)
map.put("uuid", DuHttpConfig.d.getUUID()); map.put("platform", "android"); map.put("v", DuHttpConfig.d.getAppVersion()); map.put("loginToken", DuHttpConfig.d.getLoginToken()); map.put("timestamp", String.valueOf(j2));- 注入时机:在原始参数基础上动态追加,顺序影响签名结果
- 关键参数:
timestamp使用时间戳,防重放攻击 - 配置来源:全部从
DuHttpConfig.d单例获取,需动态提取
4. 字典序排序(签名核心)
Collections.sort(arrayList, (entry, entry2) -> entry.getKey().toString().compareTo(entry2.getKey()) );- 排序规则:严格按 key 字符串升序排列
- 逆向要点:必须与服务器端排序逻辑完全一致
- 潜在坑点:不同语言的字典序实现差异(如 Java vs Python)
5. 字符串拼接
sb.append(((String) entry.getKey()) + ((String) entry.getValue()));- 格式:
key1value1key2value2...无分隔符 - 示例:
abValue1platformandroidtimestamp1762780383093... - 长度风险:超长参数可能导致签名串溢出(需服务端支持)
6. AES 加密层
AESEncrypt.encode(DuHttpConfig.f15798c, sb2)- 密钥:
DuHttpConfig.f15798c(通常为 16 字节字符串) - 模式:需动态分析确认(ECB/CBC/PKCS5Padding)
- 输出:Base64 编码字符串(进入下一步 MD5)
7. 最终 MD5 哈希
return a(AESEncrypt.encode(...));- 结果特征:32 位十六进制小写字符串
- 算法:
MD5(Base64(AES(plain)))
加密a代码分析

跟进代码发现a做的md5加密
代码详解
1. 热修复防护(Robust)
PatchProxyResult proxy = PatchProxy.proxy(..., changeQuickRedirect, true, 6616, ...); if (proxy.isSupported) { return (String) proxy.result; }- 方法编号:6616(与
c()方法的 6612 对应) - 绕过策略:Hook
PatchProxy.proxy强制返回isSupported=false
2. MD5 计算核心
MessageDigest messageDigest = MessageDigest.getInstance("MD5"); messageDigest.update(str.getBytes()); *// 默认使用 UTF-8 编码*byte[] digest = messageDigest.digest();- 哈希对象:标准 Java
MessageDigestMD5 实例 - 输入编码:
str.getBytes()使用系统默认编码(Android 默认为 UTF-8) - 输出:16 字节二进制数组
3. 字节转十六进制字符串
StringBuilder sb = new StringBuilder(); for (byte b2 : digest) { String hexString = Integer.toHexString(b2 & 255); *// 转无符号整数*while (hexString.length() < 2) { hexString = "0" + hexString; *// 补零保证 2 位*} sb.append(hexString); }- 位运算:
b2 & 255将字节转为无符号整数(0-255) - 补零逻辑:确保每个字节对应 2 位十六进制(如
0→00) - 结果:32 位小写十六进制字符串
二、完整签名算法链确认
结合
RequestUtils.c()和a(),最终算法为:String sign = MD5( Base64( AES_Encrypt( DuHttpConfig.f15798c, *// AES 密钥* 拼接字符串 *// 排序后的 key=value 无分隔符拼接*) ) )- 方法编号:6616(与

魔改测试
要确认
a()方法中的 MD5 是否魔改(自定义),需要动态+静态双重验证。以下是系统性检测方案:
一、动态验证:标准 MD5 对比法
Frida 脚本:实时比对
Java.perform(function () { const RequestUtils = Java.use('com.shizhuang.duapp.common.utils.RequestUtils'); const MessageDigest = Java.use('java.security.MessageDigest'); *// Hook a() 方法*RequestUtils.a.overload('java.lang.String').implementation = function(input) { console.log('\n[══════════════════════════════════════]'); console.log('[+] a() 方法被调用'); console.log('[+] 输入: ' + input); *// 调用原始方法*const appResult = this.a(input); console.log('[+] App MD5结果: ' + appResult); *// 使用标准Java MD5计算*const standardMD5 = calculateStandardMD5(input); console.log('[+] 标准MD5结果: ' + standardMD5); *// 对比*const isModified = (appResult !== standardMD5); console.log('[!] 是否魔改: ' + isModified); if (isModified) { console.log('[!!!] 发现魔改MD5!'); *// 打印调用栈定位魔改点*console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); } else { console.log('[✓] MD5未被修改,为标准实现'); } console.log('[══════════════════════════════════════]\n'); return appResult; }; *// 标准MD5计算函数*function calculateStandardMD5(str) { try { const md = MessageDigest.getInstance('MD5'); md.update(str.getBytes('UTF-8')); const digest = md.digest(); const sb = []; for (let i = 0; i < digest.length; i++) { let hex = (digest[i] & 0xff).toString(16); while (hex.length < 2) hex = '0' + hex; sb.push(hex); } return sb.join(''); } catch (e) { console.log('标准MD5计算失败: ' + e); return ''; } } });验证逻辑:对相同输入,对比 App 输出和标准 Java MD5 输出,完全一致则未魔改。
二、静态分析:字节码验证
1. 检查 MD5 初始化
使用 JADX 或 GDA 反编译,确认:
`*// ✅ 标准实现(无魔改)*MessageDigest.getInstance(“MD5”)
// ❌ 可疑魔改迹象MessageDigest.getInstance(“MD5”, customProvider) // 自定义ProviderMyCustomMD5.getInstance(“MD5”) // 调用自定义类`
2. 检查是否有额外处理
在
a()方法中搜索:& 0xff或& 255→ 标准位运算- 额外位移、异或 → 可能魔改
- 调用 native 方法 → 需分析 so 文件
3. 使用 ByteCode Viewer 检查
查看
a()方法的 JVM 字节码,确认:- 是否调用
java.security.MessageDigest - 是否有插入
INVOKESTATIC调用其他工具类 - 是否有
LOOKUPSWITCH或TABLESWITCH做分支篡改
三、边界测试:用已知输入输出验证
测试用例设计
`// Frida 主动注入测试Java.perform(function () {
const RequestUtils = Java.use(‘com.shizhuang.duapp.common.utils.RequestUtils’);*// 绕过 Robust*bypassRobust(); *// 标准测试向量*const testCases = [ "", *// 空字符串*"a", *// 单字符*"abc", *// RFC 1321 标准测试*"message digest", *// 较长字符串*"abcdefghijklmnopqrstuvwxyz", *// 全字母*"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", *// 混合*"12345678901234567890123456789012345678901234567890123456789012345678901234567890" *// 超长*]; testCases.forEach((input, i) => { console.log(`\n[测试用例 ${i+1}] 输入: "${input}"`); const appResult = RequestUtils.a(input); console.log(`App结果: ${appResult}`); *// 对比标准MD5(已知值)*const standard = knownMD5(input); console.log(`标准结果: ${standard}`); console.log(`状态: ${appResult === standard ? '✅ 通过' : '❌ 失败'}`); });});
function knownMD5(str) {
// 使用已知的标准MD5值或在线计算// 例如: “” → d41d8cd98f00b204e9800998ecf8427e// 此处省略实现,建议用Python验证return “”;
}function bypassRobust() {
const PatchProxy = Java.use(‘com.meituan.robust.PatchProxy’);
PatchProxy.proxy.implementation = function() {
const result = Java.use(‘com.meituan.robust.PatchProxyResult’).$new();
result.setIsSupported(false);
return result;
};
}`
四、Native 层深度检查
确认是否调用 BouncyCastle 等第三方库
Java.perform(function () { *// Hook MessageDigest.getInstance 看实际返回的 Provider*const MessageDigest = Java.use('java.security.MessageDigest'); MessageDigest.getInstance.overload('java.lang.String').implementation = function(algorithm) { const md = this.getInstance(algorithm); console.log([+] MD5 Provider: ${md.getProvider().getName()}`);
return md;
};*// 检查 Provider 列表*const Security = Java.use('java.security.Security'); const providers = Security.getProviders(); providers.forEach(p => { console.log(`Provider: ${p.getName()} - ${p.getInfo()}`); });});`
关注点:
- Provider 名称:
AndroidOpenSSL(标准) vsBC(BouncyCastle) - Native 库:
libjavacrypto.so(标准) vslibbcprov.so(可能魔改)
五、Hook MessageDigest 本身
终极验证:监控 MD5 计算过程
`Java.perform(function () {
const MessageDigest = Java.use(‘java.security.MessageDigest’);*// Hook update 方法看输入数据*MessageDigest.update.overload('[B').implementation = function(input) { console.log('[+] MD5.update() 调用'); console.log('[+] 数据: ' + JSON.stringify(Array.from(input))); return this.update(input); }; *// Hook digest 方法看输出*MessageDigest.digest.overload().implementation = function() { const result = this.digest(); console.log('[+] MD5.digest() 调用'); console.log('[+] 输出: ' + JSON.stringify(Array.from(result))); return result; }; *// Hook a() 方法*const RequestUtils = Java.use('com.shizhuang.duapp.common.utils.RequestUtils'); RequestUtils.a.implementation = function(str) { const result = this.a(str); console.log('\n[完整流程]'); console.log('[+] a() 输入字符串: ' + str); console.log('[+] a() 输出MD5: ' + result); return result; };});`
验证逻辑:
- 如果
update()接收到的数据与a()的输入str一致 - 且
digest()输出字节数组转十六进制后与a()返回值一致 - 则 100% 确认 MD5 未被魔改
六、完整性检查清单
检查项 方法 预期结果 如果异常说明 输入一致性 Hook update()输入 = a()的 str 参数传入数据被篡改 输出一致性 Hook digest()输出转 hex = a()返回值MD5 算法被替换 Provider getProvider()AndroidOpenSSL/BC 使用自定义 Provider 字节码 JADX 分析 无额外方法调用 插入了混淆/魔改代码 边界测试 RFC 1321 测试用例 与标准值一致 算法实现有差异 Native 调用 Frida Module.findExportByName无自定义 so 调用 MD5 在 native 层魔改
七、快速验证命令
`# 1. 启动 Frida 并运行上述 Hook 脚本
frida -U -f com.shizhuang.duapp -l verify_md5.js –no-pause# 2. 触发一次网络请求(或手动调用)# 在 Frida 控制台输入:# RequestUtils.a(“test”)# 3. 对比标准 MD5echo -n “test” | md5sum # Linux# 或 Python
python -c “import hashlib; print(hashlib.md5(b’test’).hexdigest())”`如果 App 输出
098f6bcd4621d373cade4e832627b4f6(标准值),则确认无魔改。
八、结论
美团/得物 App 的
a()方法 MD5 实现是标准的,理由:- 直接调用
MessageDigest.getInstance("MD5")(官方 API) - 字节转十六进制逻辑与标准实现一致
- 无自定义 Provider 或额外处理
- 输出格式为标准 32 位小写 MD5
99% 的可能是未魔改,因为:
- 维护成本:魔改 MD5 需同步修改服务端,增加维护复杂度
- 安全性:AES 密钥和签名流程已足够安全,无需魔改哈希算法
- 兼容性:标准 MD5 保证跨平台一致性
最终确认:运行上述 Frida 对比脚本,若输入输出与标准 MD5 一致,即可100%定论。
分析AESEncrypt.encode加密算法

观察代码可以发现
1 | |
encode()
AES代码逻辑明显在so层JNIEncrypt文件中,我们先向下查看我们的encode
1 | |
而关键的getByteValues() 和 encodeByte()都是Native方法。

在so层分析之前我们可以先hook一下encode确认,需要注意的是这个类下encode是重载方法,我们hook的是两个参数类型。所以我们需要overload传参数签名进行处理。
1 | |

我们发现每次下滑可以得到两个参数和返回值

简单分解一下就可以发现,上面的str字符串更符合我们上边分析的str字符拼接。
getByteValues()
接着我们进入libJNIEncrypt.so中分析一下getByteValues() 和 encodeByte(),而getByteValues() 没有参数直接返回字符串大概率固定,我们可以直接hook验证一下。
1 | |

我们多次验证可以发现每次结果一致101001011101110101101101111100111000110100010101010111010001000101100101010010010101110111010011101001011101110101100101001100110000110100011101010111011011001101001101011101010100001101000011
不过我们要注意上面分析的时候用的值对getByteValues()进行了取反
encodeByte()
我们先找到对应的so文件进行反编译

静态注册:java_包名_类名_方法名
动态注册:JNI_onLoad中找对应关系
我们进入就看到了JNI_OnLoad,进入观察确实是我们要找的逻辑部分,我们进一步分析其对应关系

安卓逆向_6 — ndk开发jni、jni静态注册、jni_onload动态注册_android jni开发-CSDN博客

提示已经很明显对应关系就是这个地方
1 | |
我们进入查看找到我们需要分析的encodeByte()

我们进一步进入查看

emm……长这样子的话,一看就知道要改一下参数
修改后代码如下
1 | |
对该代码进行简单分析
1. JNI字符串处理
1
2
3
4
5
6
7
8v7 = (*((__int64 (__fastcall **)(const struct JNINativeInterface *, __int64, _QWORD))JNIEnv_->reserved0 + 169))(
JNIEnv_, a4, 0);// GetStringUTFChars 获取输出字符串
n0x1F = (*((__int64 (__fastcall **)(const struct JNINativeInterface *, jobject *))JNIEnv_->reserved0 + 171))(
JNIEnv_, jstring);// GetStringLength 获取输入字符串长度
v10 = (*((__int64 (__fastcall **)(const struct JNINativeInterface *, jobject *, _QWORD))JNIEnv_->reserved0 + 184))(
JNIEnv_, jstring, 0);// GetStringUTFChars 获取输入字符串内容JNI函数映射:
reserved0 + 169=GetStringUTFChars(输出字符串)reserved0 + 171=GetStringLengthreserved0 + 184=GetStringUTFChars(输入字符串)
2. 内存分配和复制优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14ptr = malloc((int)(n0x1F + 1));// 分配输入字符串缓冲区(+1用于null终止符)// 内存复制优化逻辑:if (n0x1F >= 1) {
if (n0x1F <= 0x1F || 内存重叠检查) {
// 小数据或内存重叠:使用逐字节复制
n0x1F_1 = 0;
do {
v17 = *v15++;// 逐字节复制*v14++ = v17;
} while (v16);
} else {
// 大数据(>31字节):使用128位向量化复制
n0x1F_1 = n0x1F & 0xFFFFFFE0;// 32字节对齐do {
v23 = *(v20 - 1);// 加载128位
v24 = *v20;// 加载128位*(v21 - 1) = v23;// 存储128位*v21 = v24;// 存储128位} while (n0x1F_2);
}
}3. AES加密调用
1
v18 = AES_128_ECB_PKCS5Padding_Encrypt(ptr, Value);// 调用AES加密4. 资源清理和返回
1
2
3
4free(ptr_1);// 释放输入缓冲区// 释放JNI字符串(*((void (__fastcall **)(const struct JNINativeInterface *, __int64, __int64))JNIEnv_->reserved0 + 170))(
JNIEnv_, a4, v7);// ReleaseStringUTFChars (输出)(*((void (__fastcall **)(const struct JNINativeInterface *, jobject *, unsigned __int64, _QWORD))JNIEnv_->reserved0 + 192))(
JNIEnv_, jstring, v10, 0);// ReleaseStringUTFChars (输入)// 创建返回字符串return (*((__int64 (__fastcall **)(const struct JNINativeInterface *, __int64))JNIEnv_->reserved0 + 167))(
JNIEnv_, v18);// NewStringUTF
我们可以发现加密字符串为V18,而与V18相关的关键代码为 v18 = AES_128_ECB_PKCS5Padding_Encrypt(ptr, Value);我们进入继续查看。

好乱,我么直接从后向前分析一下
很明显是通过AES按位加密后又进行了一次base64编码。
到这里,我们可以尝试hook一下AES_128_ECB_PKCS5Padding_Encrypt(ptr, Value);的参数。

1 | |

通过结果进行简单分析可以判断,因为参数2是固定的,所以猜测为AES的key值,根据之前的分析可以知道base64返回值再经过md5加密是我们抓包得到的值。我们通过python验证一下。

结果一致说明返回值正确。
我们尝试使用重写Native方法加密,发现与hook方法一致

分析uuid的获取
在上面的分析中我们已经完成了对字符拼接逻辑和AES加密的还原,但我们拼接中的uuid还不确定
abRecReason 0
abRectagFengge 0
abTypesocial_brand_strategy_v454abValue1abVideoCover2deliveryProjectId0
lastId
limit 20
loginToken
platform android
timestamp 1762868671633
uuid 06940e61ab6bfc52
v 4.74.5
我么回到字符拼接的位置,查找到uuid的方法

我么进入getUUID() 进行查看

emm……误闯天家~看不懂,思密达。扔AI里
这是一个典型的热修复框架实现:
PatchProxy.proxy()- 热修复代理调用changeQuickRedirect- 热修复重定向标志5097- 方法ID- 如果热修复支持,返回修复后的结果,否则返回空字符串
很明显在这里我们没办法看出uuid是什么无法直接调用
爆破
脚本 1:绕过补丁并获取 UUID
Java.perform(function () { const DuHttpConfig = Java.use('com.dianping.nova.core.config.DuHttpConfig'); *// 1. 绕过 Robust 框架*const PatchProxy = Java.use('com.meituan.robust.PatchProxy'); PatchProxy.proxy.overload( 'java.lang.Object', 'java.lang.Object', 'com.meituan.robust.ChangeQuickRedirect', 'boolean', 'int', 'java.lang.Class[]', 'java.lang.Class' ).implementation = function(args, instance, changeQuickRedirect, isStatic, methodNumber, paramsClasses, returnClass) { *// 强制返回未命中补丁*const result = Java.use('com.meituan.robust.PatchProxyResult').$new(); result.setIsSupported(false); return result; }; *// 2. Hook getUUID 打印调用栈*DuHttpConfig.getUUID.implementation = function() { const uuid = this.getUUID(); console.log('\n[══════════════════════════════════════]'); console.log('[+] UUID: ' + uuid); *// 打印调用栈,定位首次生成位置*console.log('[+] 调用栈:'); console.log(Java.use("android.util.Log").getStackTraceString( Java.use("java.lang.Exception").$new() )); console.log('[══════════════════════════════════════]\n'); return uuid; }; *// 3. 主动触发调用*const config = DuHttpConfig.d; console.log('[+] 主动获取 UUID: ' + config.getUUID()); });脚本 2:动态追踪 UUID 生成源头
*// 监控所有 UUID 生成相关 API*Java.perform(function () { *// 1. 监控 java.util.UUID.randomUUID()*const UUID = Java.use('java.util.UUID'); UUID.randomUUID.implementation = function() { const uuid = this.randomUUID(); console.log('[!!!] UUID.randomUUID() 生成: ' + uuid.toString()); return uuid; }; *// 2. 监控 SharedPreferences 读取*const SharedPreferences = Java.use('android.content.SharedPreferencesImpl'); SharedPreferences.getString.overload('java.lang.String', 'java.lang.String').implementation = function(key, defValue) { const ret = this.getString(key, defValue); if (key === 'device_uuid' || key.contains('uuid')) { console.log('[!!!] 从 SharedPreferences 读取 UUID: ' + ret); } return ret; }; *// 3. 监控 Settings.Secure.ANDROID_ID*const Secure = Java.use('android.provider.Settings$Secure'); Secure.getString.implementation = function(cr, name) { const ret = this.getString(cr, name); if (name === 'android_id') { console.log('[!!!] ANDROID_ID: ' + ret); } return ret; }; });
这里我们再尝试搜索别的代码uuid看是否可以生成使用。这里我们尝试搜索接下来要破解的X-Auth-Token中的uuid

我们进入a方法进行查看

到这步我们看到telephonyManager.getDeviceId()基本可以确定调用的是设备ID号。

知道uuid是获取手机的 **IMEI(强标识)**后我们对newsign方法进行整合。
IMEI
1. IMEI (International Mobile Equipment Identity)
- 格式: 15位数字
- 结构: AA-BBBBBB-CCCCCC-D
- 示例: 35-209900-176148-1
- 用途: 全球唯一标识移动设备
2. MEID (Mobile Equipment Identifier)
- 格式: 14位十六进制数字
- 结构: AA-BBBBBB-CCCCCC
- 用途: CDMA网络设备标识
1 | |

X-Auth-Token破解
三段式,使用.分割,典型的jwt认证中的token
三个部分分别是:
- Header(头部)
- 包含令牌类型(typ: JWT)和签名算法(alg: HS256/RS256等)
- Base64Url编码的JSON对象
- Payload(载荷)
- 存放声明信息(Claims),如用户ID、角色、过期时间(exp)等
- Base64Url编码的JSON对象
- Signature(签名)
- 用密钥对
header.payload进行签名,防止篡改 - 确保token完整性和真实性
- 用密钥对
Bearer
第一段:
eyJhbGciOiJSUzI1NiJ9.
第二段:
eyJpYXQiOjE3NjI5MTk4NTUsImV4cCI6MTc5NDQ1NTg1NSwiaXNzIjoiMDY5NDBlNjFhYjZiZmM1MiIsInN1YiI6IjA2OTQwZTYxYWI2YmZjNTIiLCJ1dWlkIjoiMDY5NDBlNjFhYjZiZmM1MiIsInVzZXJJZCI6MjcwODg1MzE1OSwidXNlck5hbWUiOiLlvpfniallci1YMFo0QzFQNCIsImlzR3Vlc3QiOnRydWV9.
第三段:
tcvtsELx4JTU902UmIlu_wRVD9OWREanN-JYQwozrmuVB8Rmph2bK2bqtUv_QDb7a_wDiuCjqh5E2X-bErq4mdry4FaYuidEHJZB_0hcEyCoyGSMEy0vqxPjMGumbNIvesJ8E_7xjhe-PN05LkNTmizLqFMLrZ0otF1nXYiQwSq_OOTDJoXZqTfBLpnnc0ev8k3-xTJ77Vs150n9MjmSsXj1jBx0_E3Q0QZ756ChAVXjw9mdUkyW5EPBv_MOvl8nqDT0tbZGFTs7DRuho4OrjzDatOymWAWlk4iADEELxbWcL58c3NCSM99wYlKNukK2efloSD2TDQvMHNWsXWLrSA
特点:
- 无状态,服务端无需存储session
- 可自包含用户信息
- 需注意Payload仅编码未加密,不要存放敏感数据
- 必须配合HTTPS使用,防止中间人攻击
这部分一定是后端返回,我们对第二部分进行解析

我们查找返回接口,之前一致搜素不到意识到应该是初次打开时进行的加载,我们清理缓存重新抓包

我们再响应头里得到接口返回的
Bearer eyJhbGciOiJSUzI1NiJ9.
eyJpYXQiOjE3NjI5MjE0MzUsImV4cCI6MTc5NDQ1NzQzNSwiaXNzIjoiMDY5NDBlNjFhYjZiZmM1MiIsInN1YiI6IjA2OTQwZTYxYWI2YmZjNTIiLCJ1dWlkIjoiMDY5NDBlNjFhYjZiZmM1MiIsInVzZXJJZCI6MjcwODg1MzE1OSwiaXNHdWVzdCI6dHJ1ZX0.
emPQ6kkg2r1qgqxRWVFwmS7oGm6N6rk57YV4svPmFzkaHPg8S7UTJSwP9aG86PCauS-vZ94laR6H_tBPmWOoPlg6RuPvhaW_Uw20mzGBAU-Irc18HJjyY-r3CUDmmPuBncs63aTPP7KMlQ7qjPz0lx5AHDBrslM0B9kugRtGdERzJj1LWmYLasaOSfP0sMS-bPg_lXYNmuJ42Eq68np2grHQqzE3m91_dwLYyI58pPFeGS-zRuvpQkRdpl9PXGBOpCNiOxJSsvuTlHAQhK4var7rRCklpJrA0te8JFrAI0ohTqGB0owd04xF8C1E6SmE48086DuUwDoDposjTdT-6A

可以发现,此后再抓到的包都使用这个接口
exp 是 JWT 标准中的 注册声明(Registered Claim),全称 Expiration Time(过期时间),用于定义 Token 的作废时间点。这是 JWT 安全机制的核心防线。

在此我们相当于找到了token的位置,接下来那就只需要调用这个接口来获取X-Auth-Token,就可以不断更新登录状态了。
1 | |
