QQ 扫码登录逆向分析,从抓包到算法还原

一、研究背景

1.1 为啥要研究这个

主要是好奇吧,每天都在用 QQ,但是从来没想过扫码登录背后是怎么实现的。而且网上关于这块的资料要么太老了,要么讲得不清楚,干脆自己抓包分析一下。

1.2 分析工具

  • Chrome 开发者工具(F12 大法好)
  • Fiddler 抓包
  • Python + requests 验证

二、QQ 扫码登录整体流程

先上一张整体流程图,有个大概印象:

plaintext
1┌──────────────────────────────────────────────────────────────────┐
2│ QQ 扫码登录完整流程 │
3└──────────────────────────────────────────────────────────────────┘
4
5┌─────────────┐
6│ 开始登录 │
7└──────┬──────┘
8
9 v
10┌─────────────────────────────────────┐
11│ Step 1: 访问 xlogin 页面 │
12│ URL: xui.ptlogin2.qq.com/xlogin │
13│ 目的: 获取 pt_login_sig Cookie │
14└──────────────────┬──────────────────┘
15
16 v
17┌─────────────────────────────────────┐
18│ Step 2: 请求二维码 │
19│ URL: ssl.ptlogin2.qq.com/ptqrshow │
20│ 返回: 二维码图片 + qrsig Cookie │
21└──────────────────┬──────────────────┘
22
23 v
24┌─────────────────────────────────────┐
25│ Step 3: 计算 ptqrtoken │
26│ 算法: hash(qrsig) │
27│ 用于: 后续轮询请求的签名 │
28└──────────────────┬──────────────────┘
29
30 v
31┌─────────────────────────────────────┐
32│ Step 4: 轮询扫码状态 │
33│ URL: ssl.ptlogin2.qq.com/ptqrlogin │
34│ 返回: 状态码 + 跳转URL │
35└──────────────────┬──────────────────┘
36
37 ┌───────────┼───────────┐
38 │ │ │
39 v v v
40 ┌───────┐ ┌────────┐ ┌────────┐
41 │ 成功 │ │ 等待中 │ │ 已失效 │
42 │ (0) │ │(65/66) │ │(10009) │
43 └───┬───┘ └────┬───┘ └────────┘
44 │ │
45 │ └──> 继续轮询
46 v
47┌─────────────────────────────────────┐
48│ Step 5: 访问跳转 URL │
49│ 目的: 获取 skey, p_skey 等 Cookie │
50└──────────────────┬──────────────────┘
51
52 v
53┌─────────────────────────────────────┐
54│ Step 6: 计算 g_tk (bkn) │
55│ 算法: hash(skey) │
56│ 用于: 后续业务接口的鉴权 │
57└──────────────────┬──────────────────┘
58
59 v
60┌─────────────┐
61│ 登录完成 │
62└─────────────┘

整个流程看起来不复杂,但是每一步都有坑,下面一个一个说。


三、Step 1:初始化登录环境

3.1 请求分析

首先要访问腾讯的 xlogin 页面:

plaintext
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,后面轮询的时候要用。

plaintext
1Set-Cookie: pt_login_sig=xxxxxx; Domain=.qq.com; Path=/

四、Step 2:获取二维码

4.1 请求分析

plaintext
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

plaintext
1Set-Cookie: qrsig=xxxxxx; Domain=.qq.com; Path=/

这个 qrsig 非常重要,是后面计算 ptqrtoken 的关键。

4.3 二维码内容

扫描二维码会得到一个 URL,格式大概是:

plaintext
1https://ssl.ptlogin2.qq.com/ptqrlogin?xxx

手机 QQ 扫描后会访问这个 URL 完成授权。


五、Step 3:ptqrtoken 算法分析

5.1 算法来源

这个算法是从腾讯的 JS 代码里扒出来的。打开浏览器开发者工具,在 Sources 面板搜索 ptqrtoken,能找到这段代码:

javascript
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 实现就是:

python
1def get_ptqrtoken(qrsig: str) -> int:
2 hash_val = 0
3 for char in qrsig:
4 hash_val += (hash_val << 5) + ord(char)
5 return hash_val & 2147483647

逐步分析:

  1. 初始化 hash_val = 0
  2. 遍历 qrsig 的每个字符
  3. hash_val << 5 等价于 hash_val * 32
  4. 加上当前字符的 ASCII 码值
  5. 最后和 0x7FFFFFFF(2147483647)做与运算

5.3 为什么要做与运算

& 2147483647 的作用是:

  1. 保证结果是正整数(去掉符号位)
  2. 限制结果在 32 位整数范围内
  3. 防止溢出

5.4 计算示例

假设 qrsig = "abc":

plaintext
1初始: hash_val = 0
2
3第1轮 (char = 'a', ASCII = 97):
4 hash_val = 0 + (0 << 5) + 97 = 97
5
6第2轮 (char = 'b', ASCII = 98):
7 hash_val = 97 + (97 << 5) + 98 = 97 + 3104 + 98 = 3299
8
9第3轮 (char = 'c', ASCII = 99):
10 hash_val = 3299 + (3299 << 5) + 99 = 3299 + 105568 + 99 = 108966
11
12最终: 108966 & 2147483647 = 108966

六、Step 4:轮询扫码状态

6.1 请求分析

plaintext
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 格式:

javascript
1ptuiCB('状态码','0','跳转URL','0','提示信息','昵称')

6.3 状态码对照表

状态码 含义 处理方式
0 登录成功 提取跳转URL,进入下一步
65 已扫描,待确认 继续轮询
66 二维码未失效 继续轮询
67 等待扫描 继续轮询
10009 二维码已失效 重新获取二维码
10006 二维码已失效 重新获取二维码

6.4 响应示例

等待扫描:

javascript
1ptuiCB('67','0','','0','二维码未失效。','')

已扫描待确认:

javascript
1ptuiCB('65','0','','0','二维码已扫描,请在手机上确认登录。','张三')

登录成功:

javascript
1ptuiCB('0','0','https://ssl.ptlogin2.qq.com/check_sig?pttype=1&uin=123456789&service=...','0','登录成功!','张三')

6.5 提取跳转 URL

登录成功后需要用正则提取跳转 URL:

python
1import re
2match = re.search(r"ptuiCB\('0','0','([^']+)','0'", response_text)
3if match:
4 redirect_url = match.group(1)

7.1 请求分析

访问上一步拿到的跳转 URL:

plaintext
1GET https://ssl.ptlogin2.qq.com/check_sig?pttype=1&uin=xxx&service=xxx...

这个请求会经过多次 302 重定向。

7.2 重定向链路

plaintext
1ssl.ptlogin2.qq.com/check_sig
2
3 └──> ptlogin2.qun.qq.com/check_sig_v3
4
5 └──> qun.qq.com/member.html (最终页面)

在重定向过程中,会设置以下关键 Cookie:

Cookie 说明
skey .qq.com 最重要,算 g_tk 要用
p_skey .qun.qq.com 某些接口需要
pt4_token .qq.com 某些接口需要
uin .qq.com 用户QQ号

7.4 注意事项

  1. 必须设置正确的 Referer:https://ssl.ptlogin2.qq.com/
  2. 要允许自动重定向(allow_redirects=True
  3. 要用 Session 保持 Cookie

八、Step 6:g_tk (bkn) 算法分析

8.1 算法来源

同样是从腾讯 JS 里扒的,搜索 getACSRFTokeng_tk

javascript
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 实现:

python
1def get_g_tk(skey: str) -> int:
2 hash_val = 5381
3 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 的原因据说是:

  1. 5381 是质数
  2. 实验证明这个值的哈希分布效果好

反正腾讯用的就是这个,别问为什么,问就是玄学。

8.5 计算示例

假设 skey = "@abc":

plaintext
1初始: hash_val = 5381
2
3第1轮 (char = '@', ASCII = 64):
4 hash_val = 5381 + (5381 << 5) + 64
5 = 5381 + 172192 + 64
6 = 177637
7
8第2轮 (char = 'a', ASCII = 97):
9 hash_val = 177637 + (177637 << 5) + 97
10 = 177637 + 5684384 + 97
11 = 5862118
12
13... 以此类推

九、完整时序图

plaintext
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/

必须用 Session 保持 Cookie,不能每次请求都新建连接。

python
1# 错误做法
2requests.get(url1)
3requests.get(url2) # Cookie 丢了
4
5# 正确做法
6session = requests.Session()
7session.get(url1)
8session.get(url2) # Cookie 自动带上

10.4 时间戳精度

action 参数要用毫秒级时间戳:

python
1# 错误
2action = f"0-0-{int(time.time())}" # 秒级,会失败
3
4# 正确
5action = f"0-0-{int(time.time() * 1000)}" # 毫秒级

10.5 User-Agent

用默认的 Python UA 可能会被拦截,建议伪装成浏览器:

python
1headers = {
2 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..."
3}

十一、总结

QQ 扫码登录的核心就两个算法:

  1. ptqrtoken:用 qrsig 算,初始值 0
  2. g_tk/bkn:用 skey 算,初始值 5381

两个算法本质上是一样的,都是 DJB2 哈希的变种,只是初始值不同。

整个流程说白了就是:

  1. 拿二维码和 qrsig
  2. 算 ptqrtoken,轮询等用户扫码
  3. 扫码成功拿跳转 URL
  4. 访问跳转 URL 拿 skey
  5. 用 skey 算 g_tk,完事儿

腾讯的接口没有文档,全靠抓包分析,而且随时可能更新。如果哪天突然不能用了,大概率是腾讯又改接口了,重新抓包分析吧。


研究这玩意花了我一个周末,期间无数次想砸电脑。 不过搞明白之后还是挺有成就感的,至少知道了每天用的 QQ 背后是怎么运作的。 希望这篇分析能帮到同样在研究这块的兄弟们,少走点弯路。 就这样,我继续去听歌摸鱼了。
© 2024 - 2026 lansonsam ,采用 CC BY-NC-SA 4.0 许可
RSS / 网站地图
AstroFuwari 强力驱动
本网站代码 已开源