QQ 扫码登录逆向分析,从抓包到算法还原
一、研究背景
1.1 为啥要研究这个
主要是好奇吧,每天都在用 QQ,但是从来没想过扫码登录背后是怎么实现的。而且网上关于这块的资料要么太老了,要么讲得不清楚,干脆自己抓包分析一下。
1.2 分析工具
- Chrome 开发者工具(F12 大法好)
- Fiddler 抓包
- Python + requests 验证
二、QQ 扫码登录整体流程
先上一张整体流程图,有个大概印象:
1┌──────────────────────────────────────────────────────────────────┐2│ QQ 扫码登录完整流程 │3└──────────────────────────────────────────────────────────────────┘4 5┌─────────────┐6│ 开始登录 │7└──────┬──────┘8 │9 v10┌─────────────────────────────────────┐11│ Step 1: 访问 xlogin 页面 │12│ URL: xui.ptlogin2.qq.com/xlogin │13│ 目的: 获取 pt_login_sig Cookie │14└──────────────────┬──────────────────┘15 │16 v17┌─────────────────────────────────────┐18│ Step 2: 请求二维码 │19│ URL: ssl.ptlogin2.qq.com/ptqrshow │20│ 返回: 二维码图片 + qrsig Cookie │21└──────────────────┬──────────────────┘22 │23 v24┌─────────────────────────────────────┐25│ Step 3: 计算 ptqrtoken │26│ 算法: hash(qrsig) │27│ 用于: 后续轮询请求的签名 │28└──────────────────┬──────────────────┘29 │30 v31┌─────────────────────────────────────┐32│ Step 4: 轮询扫码状态 │33│ URL: ssl.ptlogin2.qq.com/ptqrlogin │34│ 返回: 状态码 + 跳转URL │35└──────────────────┬──────────────────┘36 │37 ┌───────────┼───────────┐38 │ │ │39 v v v40 ┌───────┐ ┌────────┐ ┌────────┐41 │ 成功 │ │ 等待中 │ │ 已失效 │42 │ (0) │ │(65/66) │ │(10009) │43 └───┬───┘ └────┬───┘ └────────┘44 │ │45 │ └──> 继续轮询46 v47┌─────────────────────────────────────┐48│ Step 5: 访问跳转 URL │49│ 目的: 获取 skey, p_skey 等 Cookie │50└──────────────────┬──────────────────┘51 │52 v53┌─────────────────────────────────────┐54│ Step 6: 计算 g_tk (bkn) │55│ 算法: hash(skey) │56│ 用于: 后续业务接口的鉴权 │57└──────────────────┬──────────────────┘58 │59 v60┌─────────────┐61│ 登录完成 │62└─────────────┘
整个流程看起来不复杂,但是每一步都有坑,下面一个一个说。
三、Step 1:初始化登录环境
3.1 请求分析
首先要访问腾讯的 xlogin 页面:
1GET https://xui.ptlogin2.qq.com/cgi-bin/xlogin
请求参数:
| 参数 | 示例值 | 说明 |
|---|---|---|
| appid | 715030901 | 应用ID,不同业务不一样 |
| daid | 73 | 域ID,和 appid 配套 |
| s | 8 | 固定值 |
| pt_3rd_aid | 0 | 第三方应用ID |
3.2 appid 对照表
不同的 QQ 业务有不同的 appid,这个是抓包抓出来的:
| 业务 | appid | daid |
|---|---|---|
| QQ群管理 | 715030901 | 73 |
| QQ空间 | 549000912 | 5 |
| QQ邮箱 | 522005705 | 4 |
| 腾讯文档 | 1006102 | 461 |
用错 appid 会导致后面拿到的 Cookie 在对应业务上用不了,这个坑我踩过。
3.3 响应分析
这个请求主要是为了在 Cookie 里设置 pt_login_sig,后面轮询的时候要用。
1Set-Cookie: pt_login_sig=xxxxxx; Domain=.qq.com; Path=/
四、Step 2:获取二维码
4.1 请求分析
1GET https://ssl.ptlogin2.qq.com/ptqrshow
请求参数:
| 参数 | 示例值 | 说明 |
|---|---|---|
| appid | 715030901 | 应用ID |
| e | 2 | 二维码类型 |
| l | M | 二维码大小(M=中等) |
| s | 3 | 样式 |
| d | 72 | 边距像素 |
| v | 4 | 版本 |
| t | 1703123456.789 | 时间戳,防缓存 |
| daid | 73 | 域ID |
| pt_3rd_aid | 0 | 第三方应用ID |
4.2 响应分析
响应体是二维码的 PNG 图片数据,同时在 Cookie 里设置 qrsig:
1Set-Cookie: qrsig=xxxxxx; Domain=.qq.com; Path=/
这个 qrsig 非常重要,是后面计算 ptqrtoken 的关键。
4.3 二维码内容
扫描二维码会得到一个 URL,格式大概是:
1https://ssl.ptlogin2.qq.com/ptqrlogin?xxx
手机 QQ 扫描后会访问这个 URL 完成授权。
五、Step 3:ptqrtoken 算法分析
5.1 算法来源
这个算法是从腾讯的 JS 代码里扒出来的。打开浏览器开发者工具,在 Sources 面板搜索 ptqrtoken,能找到这段代码:
1function getptqrtoken(qrsig) {2 var e = 0;3 for (var i = 0; i < qrsig.length; i++) {4 e += (e << 5) + qrsig.charCodeAt(i);5 }6 return e & 2147483647;7}
5.2 算法解析
用 Python 实现就是:
1def get_ptqrtoken(qrsig: str) -> int:2 hash_val = 03 for char in qrsig:4 hash_val += (hash_val << 5) + ord(char)5 return hash_val & 2147483647
逐步分析:
- 初始化
hash_val = 0 - 遍历 qrsig 的每个字符
hash_val << 5等价于hash_val * 32- 加上当前字符的 ASCII 码值
- 最后和
0x7FFFFFFF(2147483647)做与运算
5.3 为什么要做与运算
& 2147483647 的作用是:
- 保证结果是正整数(去掉符号位)
- 限制结果在 32 位整数范围内
- 防止溢出
5.4 计算示例
假设 qrsig = "abc":
1初始: hash_val = 02 3第1轮 (char = 'a', ASCII = 97):4 hash_val = 0 + (0 << 5) + 97 = 975 6第2轮 (char = 'b', ASCII = 98):7 hash_val = 97 + (97 << 5) + 98 = 97 + 3104 + 98 = 32998 9第3轮 (char = 'c', ASCII = 99):10 hash_val = 3299 + (3299 << 5) + 99 = 3299 + 105568 + 99 = 10896611 12最终: 108966 & 2147483647 = 108966
六、Step 4:轮询扫码状态
6.1 请求分析
1GET https://ssl.ptlogin2.qq.com/ptqrlogin
这个接口参数巨多,我整理了一下:
| 参数 | 示例值 | 说明 |
|---|---|---|
| u1 | https://qun.qq.com/member.html | 登录成功后跳转地址 |
| ptqrtoken | 123456789 | 上一步算出来的 |
| ptredirect | 0 | 固定值 |
| h | 1 | 固定值 |
| t | 1 | 固定值 |
| g | 1 | 固定值 |
| from_ui | 1 | 固定值 |
| ptlang | 2052 | 语言代码,2052=简体中文 |
| action | 0-0-1703123456789 | 格式: 0-0-毫秒时间戳 |
| js_ver | 24051615 | JS版本号,会变 |
| js_type | 1 | 固定值 |
| login_sig | xxx | 从 Cookie 获取 |
| pt_uistyle | 40 | UI样式 |
| aid | 715030901 | appid |
| daid | 73 | 域ID |
6.2 响应格式
响应是 JSONP 格式:
1ptuiCB('状态码','0','跳转URL','0','提示信息','昵称')
6.3 状态码对照表
| 状态码 | 含义 | 处理方式 |
|---|---|---|
| 0 | 登录成功 | 提取跳转URL,进入下一步 |
| 65 | 已扫描,待确认 | 继续轮询 |
| 66 | 二维码未失效 | 继续轮询 |
| 67 | 等待扫描 | 继续轮询 |
| 10009 | 二维码已失效 | 重新获取二维码 |
| 10006 | 二维码已失效 | 重新获取二维码 |
6.4 响应示例
等待扫描:
1ptuiCB('67','0','','0','二维码未失效。','')
已扫描待确认:
1ptuiCB('65','0','','0','二维码已扫描,请在手机上确认登录。','张三')
登录成功:
1ptuiCB('0','0','https://ssl.ptlogin2.qq.com/check_sig?pttype=1&uin=123456789&service=...','0','登录成功!','张三')
6.5 提取跳转 URL
登录成功后需要用正则提取跳转 URL:
1import re2match = re.search(r"ptuiCB\('0','0','([^']+)','0'", response_text)3if match:4 redirect_url = match.group(1)
七、Step 5:获取关键 Cookie
7.1 请求分析
访问上一步拿到的跳转 URL:
1GET https://ssl.ptlogin2.qq.com/check_sig?pttype=1&uin=xxx&service=xxx...
这个请求会经过多次 302 重定向。
7.2 重定向链路
1ssl.ptlogin2.qq.com/check_sig2 │3 └──> ptlogin2.qun.qq.com/check_sig_v34 │5 └──> qun.qq.com/member.html (最终页面)
7.3 获取的 Cookie
在重定向过程中,会设置以下关键 Cookie:
| Cookie | 域 | 说明 |
|---|---|---|
| skey | .qq.com | 最重要,算 g_tk 要用 |
| p_skey | .qun.qq.com | 某些接口需要 |
| pt4_token | .qq.com | 某些接口需要 |
| uin | .qq.com | 用户QQ号 |
7.4 注意事项
- 必须设置正确的 Referer:
https://ssl.ptlogin2.qq.com/ - 要允许自动重定向(
allow_redirects=True) - 要用 Session 保持 Cookie
八、Step 6:g_tk (bkn) 算法分析
8.1 算法来源
同样是从腾讯 JS 里扒的,搜索 getACSRFToken 或 g_tk:
1function getACSRFToken(skey) {2 var hash = 5381;3 for (var i = 0; i < skey.length; i++) {4 hash += (hash << 5) + skey.charCodeAt(i);5 }6 return hash & 2147483647;7}
8.2 算法解析
Python 实现:
1def get_g_tk(skey: str) -> int:2 hash_val = 53813 for char in skey:4 hash_val += (hash_val << 5) + ord(char)5 return hash_val & 2147483647
8.3 和 ptqrtoken 的区别
| ptqrtoken | g_tk | |
|---|---|---|
| 输入 | qrsig | skey |
| 初始值 | 0 | 5381 |
| 算法 | 相同 | 相同 |
唯一的区别就是初始值,ptqrtoken 是 0,g_tk 是 5381。
8.4 为什么是 5381
5381 是 DJB2 哈希算法的魔数,这个算法是 Daniel J. Bernstein 发明的。选择 5381 的原因据说是:
- 5381 是质数
- 实验证明这个值的哈希分布效果好
反正腾讯用的就是这个,别问为什么,问就是玄学。
8.5 计算示例
假设 skey = "@abc":
1初始: hash_val = 53812 3第1轮 (char = '@', ASCII = 64):4 hash_val = 5381 + (5381 << 5) + 645 = 5381 + 172192 + 646 = 1776377 8第2轮 (char = 'a', ASCII = 97):9 hash_val = 177637 + (177637 << 5) + 9710 = 177637 + 5684384 + 9711 = 586211812 13... 以此类推
九、完整时序图
1┌────────┐ ┌────────────────┐ ┌────────────────┐2│ Client │ │ QQ Login API │ │ QQ Mobile │3└───┬────┘ └───────┬────────┘ └───────┬────────┘4 │ │ │5 │ GET /xlogin │ │6 │──────────────────────>│ │7 │ │ │8 │ Set-Cookie: │ │9 │ pt_login_sig │ │10 │<──────────────────────│ │11 │ │ │12 │ GET /ptqrshow │ │13 │──────────────────────>│ │14 │ │ │15 │ 二维码图片 + │ │16 │ Set-Cookie: qrsig │ │17 │<──────────────────────│ │18 │ │ │19 │ 计算 ptqrtoken │ │20 │ = hash(qrsig) │ │21 │ │ │22 │ GET /ptqrlogin │ │23 │ (轮询) │ │24 │──────────────────────>│ │25 │ │ │26 │ ptuiCB('67'...) │ │27 │ 等待扫描 │ │28 │<──────────────────────│ │29 │ │ │30 │ │ 用户扫码 │31 │ │<──────────────────────────│32 │ │ │33 │ GET /ptqrlogin │ │34 │──────────────────────>│ │35 │ │ │36 │ ptuiCB('65'...) │ │37 │ 已扫描待确认 │ │38 │<──────────────────────│ │39 │ │ │40 │ │ 用户确认 │41 │ │<──────────────────────────│42 │ │ │43 │ GET /ptqrlogin │ │44 │──────────────────────>│ │45 │ │ │46 │ ptuiCB('0'...) │ │47 │ 登录成功 + 跳转URL │ │48 │<──────────────────────│ │49 │ │ │50 │ GET /check_sig │ │51 │ (跳转URL) │ │52 │──────────────────────>│ │53 │ │ │54 │ 302 重定向 │ │55 │ Set-Cookie: │ │56 │ skey, p_skey... │ │57 │<──────────────────────│ │58 │ │ │59 │ 计算 g_tk │ │60 │ = hash(skey) │ │61 │ │ │62 │ 登录完成,可调用 │ │63 │ 业务接口 │ │64 │ │ │
十、踩坑记录
10.1 js_ver 过期
腾讯会不定期更新 JS 版本号,如果发现登录突然失败,可以试试更新 js_ver 参数。
获取方法:打开 QQ 登录页面,在 Network 里找 ptqrlogin 请求,看它带的 js_ver 是多少。
10.2 Referer 检查
腾讯对 Referer 检查很严格,每个请求都要设置正确的 Referer,不然会返回错误。
| 请求 | Referer |
|---|---|
| ptqrshow | https://xui.ptlogin2.qq.com/ |
| ptqrlogin | https://xui.ptlogin2.qq.com/ |
| check_sig | https://ssl.ptlogin2.qq.com/ |
10.3 Cookie 丢失
必须用 Session 保持 Cookie,不能每次请求都新建连接。
1# 错误做法2requests.get(url1)3requests.get(url2) # Cookie 丢了4 5# 正确做法6session = requests.Session()7session.get(url1)8session.get(url2) # Cookie 自动带上
10.4 时间戳精度
action 参数要用毫秒级时间戳:
1# 错误2action = f"0-0-{int(time.time())}" # 秒级,会失败3 4# 正确5action = f"0-0-{int(time.time() * 1000)}" # 毫秒级
10.5 User-Agent
用默认的 Python UA 可能会被拦截,建议伪装成浏览器:
1headers = {2 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..."3}
十一、总结
QQ 扫码登录的核心就两个算法:
- ptqrtoken:用 qrsig 算,初始值 0
- g_tk/bkn:用 skey 算,初始值 5381
两个算法本质上是一样的,都是 DJB2 哈希的变种,只是初始值不同。
整个流程说白了就是:
- 拿二维码和 qrsig
- 算 ptqrtoken,轮询等用户扫码
- 扫码成功拿跳转 URL
- 访问跳转 URL 拿 skey
- 用 skey 算 g_tk,完事儿
腾讯的接口没有文档,全靠抓包分析,而且随时可能更新。如果哪天突然不能用了,大概率是腾讯又改接口了,重新抓包分析吧。
研究这玩意花了我一个周末,期间无数次想砸电脑。 不过搞明白之后还是挺有成就感的,至少知道了每天用的 QQ 背后是怎么运作的。 希望这篇分析能帮到同样在研究这块的兄弟们,少走点弯路。 就这样,我继续去听歌摸鱼了。