大家应该都体验过腾讯系应用的一键登录功能(PC),当你的电脑上登陆了 QQ 之后,使用QQ快捷登录进行OAuth验证时不需要再手动输入账号和密码,点击自己的头像就可以成功登陆应用。

image-20230422171158130

也许你也曾有过好奇她是如何实现的,今天我们就来复刻一个。

原理剖析

当我们登陆QQ的时候有快捷登录,没登QQ的时候只有扫码登录,一定适合我们的电脑QQ进行了通信,但是浏览器权限非常低,在没有弹窗授权的情况下不可能直接和应用进行通信,可以猜一手通过HTTP我们可以使用Chrome的网络抓包工具来看一下到底是怎么实现的

image-20230422213914949

经过仔细检查最终发现了这样一个请求,看样子时JSONP类型的请求,他的响应格式如下

var var_sso_uin_list = [{
  "uin": ------,
  "face_index": 0,
  "gender": 0,
  "nickname": "沿途の风景",
  "client_type": ------,
  "uin_flag": ------,
  "account": ------
}];
ptui_getuins_CB(var_sso_uin_list);

再来看这个请求的地址,第一感觉就是请求了localhost,可以ping一下看一看IP

image-20230422214229926

看样子腾讯是将这个域名解析到了127.0.0.1,调用该域名的接口时就会访问用户本机,而这个域名后面还有一个端口,我们再检查一下本机的4301端口

image-20230422214424622

果不其然,看来QQ客户端就是监听了某个端口,快捷登录的数据就是从这里获取的。

剩下的就是OAuth的东西了,我们就不转门去研究了,这个应该是和QQ互联的借口相关。

复刻

在知道实现原理之后我们就可以动手实现一个了,我们选择给予Electron来实现一个客户端(JS是万能的)。

首先初始化一个Electron的项目(不会Electron的可以简单了解一下,我后面会出一个Electron的学习笔记)。

可以利用NodeJS的内置模块http来启动一个服务,监听某一个端口,将对应路径的请求返回即可,http服务的代码如下

import { createServer } from 'http'
import { parse as parseUrl } from 'url'
import { parse as parseQs } from 'querystring'

let server;

export function openServer() {
  closeServer();

  server = createServer((req, res) => {
    const url = req.url
    const urlObj = parseUrl(url)
    const qs = parseQs(urlObj.query)

    if (urlObj.pathname === '/pt_get_uins') {
      res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' })
      res.end(
        'var var_sso_uin_list = [{' +
        '"uin": 123456,' +
        '"face_index": 0,' +
        '"gender": 0,' +
        '"nickname": "沿途の风景",' +
        '"client_type": 123456,' +
        '"uin_flag": 123456,' +
        '"account": 123456' +
        '}];' + qs.callback +
        '(var_sso_uin_list);'
      )
    } else {
      res.writeHead(400)
      res.end('Not Found')
    }
  })

  server.listen(8888)
}

export function closeServer() {
  server && server.removeAllListeners();
  server && server.close(() => {
    console.log("服务接口关闭");
  });
}

在这段代码中,我们启动了一个httpServer,这个server中针对指定的路径返回了之前我们解析出的QQ返回的文件格式,给响应设置了Content-Type为javascript,拼接了请求中传递的callback函数。

我们将这段代码添加到主进程代码中

import { app, BrowserWindow } from 'electron';
import { openServer } from './httpServer';

let mainWindow: BrowserWindow | null;

app.whenReady().then(() => {
  mainWindow = new BrowserWindow({});
  mainWindow.loadURL(process.argv[2]);
  openServer()
})

这时启动应用即可体验效果,打开浏览器发送访问路径即可

image-20230427223104722

具体的嵌入到应用中我们就不模拟了,就是将这个请求嵌入到业务组件中即可。


前端小白