你了解扫码登录的本质和原理吗?
你了解扫码登录的本质和原理吗?
我们日常生活中会使用到各种各样的系统,比如微信,qq ,哔哩哔哩等等。这些系统都是要求登录的,登录的方式也有好几种方式,常见的登录方式有表单登录,扫码登录,移动端三方授权登录等。
登录的本质是什么呢?我认为登录的本质就是服务器确认用户的身份,并且授予客户端身份证,比如token,sessionId。用户确认身份后才进行进一步的操作,比如订单功能,收藏功能等等。
本文介绍一下扫码登录的原理。
首先说一下什么是二维码,百度百科定义:二维条码/二维码是用某种特定的几何图形按一定规律在平面(二维方向上)分布的、黑白相间的、记录数据符号信息的图形本文详细的讲述扫码登录的方式。
通俗讲就是我们按照特定的算法可以把字符或者其他信息生成二维码,也可以通过解码的方式把二维码中信息解析出来。
常见的扫码登录案例
1. 微信授权登录(微信网页版)
微信授权登录(微信网页版)注:上图中 二维码内容 https://login.weixin.qq.com/l/iZuktsOzbA===
采用长轮训的请求方式,图中25.19s是请求的时间(服务器25s才放弃前一次连接)
2. 微信开放平台授权
微信开放平台授权上图中二维码内容 https://open.weixin.qq.com/connect/confirm?uuid=08119Fpi3VQb0w3S
采用长轮训的请求方式,图中15.15s是请求的时间
3. 扫码关注公众号快速登录 (微信公众号自定义服务器)
扫码关注公众号快速登录 (微信公众号自定义服务器)上图中二维码内容 http://weixin.qq.com/q/029pRtV2xhf5i1g3wEhvcw
采用短轮训的请求方式,图中请求时间平均50ms,一秒钟时间请求多次
4. 厂商自定义扫码登录(爱奇艺系统自己实现逻辑)
厂商自定义扫码登录(爱奇艺系统自己实现逻辑)通过上面4种二维码的内容可以看出,所有的链接都会有一个uuid/token这样的系统唯一值。这个唯一值有什么作用呢?接着看下面扫码登录的原理。
扫码登录功能原理
扫码登录功能原理扫码登录的目的是浏览器端或者客户端(准确的说需要登录的客户端)获取到手机端用户的身份。
首先手机端肯定是需要登录状态的,不然咋给其他的客户端授权,这个好像是句废话。
然后移动端如果想把自己的登录身份发送给浏览器,是需要借助服务器来作为中间介质来进行身份信息传递。
数据传输过程:
-
浏览器生成一个uuid,然后根据生成的uuid生成对应的二维码(一般由前端生成,为什么需要uuid呢?因为需要登录的用户可能同时存在多个,如果没有一个系统唯一值uuid, 服务器无法确认是哪个浏览器正在等待登录,所以是需要一个uuid来区分同时登录的浏览器。)
-
移动端扫码获取到二维码的内容(uuid)
-
通过http或者其他数据传输协议向服务器提交uuid对应的授权数据,服务器端将授权数据按照一定规则进行存储。
-
二维码展示到浏览器的同时,浏览器通过不同的方式(短轮训,长轮训,websocket等)向服务器请求是否有uuid对应的授权信息。
4.1 短轮训:ajax定时请求服务器接口,直到获取到授权信息,或者超时,服务器端立刻返回请求。(扫码关注公众号快速登录采用这种方式)
4.2 长轮训:ajax定时请求服务器接口,直到获取到授权信息,或者超时,服务器端会先hold住请求,直到服务器收到授权信息或者服务器请求超时。(微信授权的两种方式都是采用这种方式)
4.3 websocket: websocket 最大的特点是服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,http协议是只能由客户端发起,每次发起请求都会进行一些重复的连接,消耗性能。浏览器采用websocket协议与服务器进行连接,服务器收到授权信息后可以主动给浏览器推送授权信息。(下面我们采用这种方式来实现一个自己的扫码登录功能)
-
获取到授权信息,进行下一步处理,比如设置把token或者sessionid等身份信息,如此浏览器就登录成功了。
实现自己的扫码登录功能
通过 Node + ws + React 技术,然后按照上面描述的扫码登录过程,来简单实现一下扫码登录的过程。
先看下效果图:
浏览器
浏览器手机端
手机端 浏览器已扫描状态代码实现
浏览器(前端)核心代码
jsimport React, { Component } from 'react'; import ReconnectingWebSocket from 'reconnecting-websocket'; //生成uuid的方法 function _uuid() { function s4() { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); } return s4() + s4() + s4() + s4() + s4() + s4() + s4() + s4(); } componentDidMount() { let { uid } = this.state; console.log("uid:", uid) //初始化 websocket 连接 let wsUrl = `${wsBaseUrl}/login/scan?uid=${uid}` const rws = new ReconnectingWebSocket(wsUrl); //对 ws 的返回数据进行监听 rws.addEventListener("message", (message) => { //获取到服务器推动到浏览器的数据 let { data } = message; data = JSON.parse(data); //扫码过程中推送信息分为两种,第一种是扫描成功,第二种是授权登录 switch (data.operate) { //扫描成功信号 case "scan": const { nickName, avatarUrl } = data; this.setState({ scan: true, text: "扫码成功", nickName, avatarUrl }); break; // 授权成功信息 case "done": this.setState({ done: true, text: "授权成功" }, () => { setTimeout(() => { //设置授权的token localStorage.setItem("TOKEN", data.token); rws.close(); //进行页面跳转 window.location.href = "/"; }, 1000); }); break; } }) }
服务器端核心代码
jsconst http = require('http'); const url = require("url"); const WebSocket = require('ws'); const qr = require('qr-image'); //登陆 ws 缓存集合 const loginMap = new Map(); // http服务器初始化 const server = http.createServer(function (request, response) { const customUrl = url.parse(request.url).pathname; switch (customUrl) { //二维码照片生成接口 case "/login/qrcode": qrImages(request, response); break; } }); //登陆二维码生成方法 function qrImages(request, response) { const uid = url.parse(request.url).query.split("=")[1]; var img = qr.image(uid, { size: 10 }); response.writeHead(200, { 'Content-Type': 'image/png' }); var responseData = [];//存储文件流 if (img) {//判断状态 img.on('data', function (chunk) { responseData.push(chunk); }); img.on('end', function () { var finalData = Buffer.concat(responseData); response.write(finalData); response.end(); }); } } //移动端扫码情况接口 function scan(request, response) { const { operate } = request.params; const { uid } = request.body; const openid = request.openid; const { data: users } = await UserDB.where({ openid }).get(); const [user] = users; let result = {}; switch (operate) { case "author": let token = AuthCustom.sigin(openid); Object.assign(result, { uid, token, operate: "done" }); break; case "scan": Object.assign(result, { uid, operate: "scan", ...user }); break; } //ws 向前端推送移动端的操作数据 loginMap.get("uid").send(result); return new Result(); } //初始化 服务的 ws 实例 const ws = new WebSocket.Server({ server }); //websocket 连接监听 ws.on('connection', function connection(ws, req) { const customUrl = url.parse(req.url).pathname; console.log("customUrl:", customUrl) switch (customUrl) { // 前端连接 ws 的接口路由,定义不同的ws路由,可以处理不同的ws case "/login/scan": scanLogin(ws, req); break } }); //登录 ws 处理 function scanLogin(ws, req) { const uid = url.parse(req.url, true).query.uid; console.log("uid:", uid); loginMap.set(uid, ws); //关闭连接 ws.on("close", () => { loginMap.delete(uid); }); } server.listen(8800, () => { console.log("app started on 8800") });
贴一下完整代码地址: https://github.com/levenx/levenx-shop
这里仅仅是完成了一个最简单的扫码登录,真正应用到生产环境还需要考虑其他的安全,性能,效率问题。后面找机会完善这些问题,敬请期待。