QQ如何实现跨端通信的

前言

这个问题之前很好解决,使用浏览器 plugin 即可。

但是随着 Chrome 和 Firefox 都先后放弃了 NPAPI plugin,这一方法也行不通了,而且很多人是很讨厌 plugin 的。

但是在默认禁止了 NPAPI 的 Chrome 版本,QQ 依然可以实现快速登录(一键登录),是怎么做到的呢?

原理

其实不难猜。既然不存在 plugin,无法以此来实现浏览器内和本地客户端的直接通信,那么排除其他的黑科技,有一种很简单的方法可以实现这个效果。

那就是在客户端开一个 Server,在浏览器里面请求这个地址。

理论上这样是可以实现的,至于 QQ 是不是用的这种方法,稍微验证下好了。

验证

找一个有 QQ 快速登陆的页面,比如 mail.qq.com登陆 QQ 客户端打开浏览器的 Developer Tools -> Network 刷新页面,观察所有请求的 domain。 很明显,我们要找的完整请求 url 是这样的

https://localhost.ptlogin2.qq.com:4301/pt_get_uins?callback=ptui_getuins_CB&r=0.125114&pt_local_tk=-2004781

看看这个请求的 Response Content

1
2
3
4
5
6
7
8
9
10
11
12
var var_sso_uin_list=[
{
"account":"xxxxxx",
"client_type":65793,
"face_index":0,
"gender":1,
"nickname":"xxx",
"uin":"xxx",
"uin_flag":xxxxx
}
];
ptui_getuins_CB(var_sso_uin_list);

很明显是当前登录的用户信息, ping 一下这个请求的 domain, 不出所料结果是 127.0.0.1

1
2
3
ping localhost.ptlogin2.qq.com

// Pinging localhost.ptlogin2.qq.com [127.0.0.1] with 32 bytes of data

现在我们验证下是否是 QQ 开了这个 Server

查看哪个程序占用了 4301 端口

1
2
3
netstat -ano | findstr "4301"

// TCP 127.0.0.1:4301 0.0.0.0:0 LISTENING 4152

得到 pid 我们就可以看否是 QQ 在监听这个端口了

1
2
3
tasklist | findstr "4152"

// QQ.exe 4152 Console 1 178,616 K

可能有人担心会不会有安全问题,会不会其他网站访问这个 url 就拿走用户信息?其实挺容易解决,存一个 token 到服务器端,获取的时候校验下就好了。

但是归根到底取决于腾讯对这方面安全的重视程度和意愿了,至少之前是确实存在从网页上获取当前登录的 QQ 信息的方法,虽然问题不是出在快速登录这部分。

模拟

接下来我用GOLang模拟这种技术实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

package main

import (
"fmt"
"log"
"io"
"net"
"net/http"
"encoding/json"
)


type Data struct{
Ip string
Mac string
}
type Ret struct{
Code int
Msg string
Data Data
}

func getMacAddrs() (macAddrs []string) {
netInterfaces, err := net.Interfaces()
if err != nil {
fmt.Printf("fail to get net interfaces: %v", err)
return macAddrs
}

for _, netInterface := range netInterfaces {
macAddr := netInterface.HardwareAddr.String()
if len(macAddr) == 0 {
continue
}

macAddrs = append(macAddrs, macAddr)
}
return macAddrs
}

func getIPs() (ips []string) {

interfaceAddr, err := net.InterfaceAddrs()
if err != nil {
fmt.Printf("fail to get net interface addrs: %v", err)
return ips
}

for _, address := range interfaceAddr {
ipNet, isValidIpNet := address.(*net.IPNet)
if isValidIpNet && !ipNet.IP.IsLoopback() {
if ipNet.IP.To4() != nil {
ips = append(ips, ipNet.IP.String())
}
}
}
return ips
}

func handleToken(w http.ResponseWriter, r *http.Request) {

log.Fatal("Request: ", r)

data := Data{Ip: getIPs()[0], Mac: getMacAddrs()[0]}

ret := new(Ret)

ret.Code = 0
ret.Msg = "success"
ret.Data = data
ret_json,_ := json.Marshal(ret)
w.Header().Set("Content-Type","text/json;charset=utf-8")
io.WriteString(w, string(ret_json))
}

func main() {
http.HandleFunc("/token", handleToken)
err := http.ListenAndServe("localhost.ya2.top:1024", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

其中域名 localhost.ya2.top 配置指向解析记录:127.0.0.1

测试效果:

搞定,这只是一个简单的DEMO,但是了解原理已经足够了。 欢迎大佬指正~

关注作者公众号,获取更多资源!
赏作者一杯咖啡~