以微信网页版为例,学习一下二维码登陆的原理。
[toc]
扫码登陆原理
以微信网页版为例,学习一下二维码登陆的原理。
浏览器打开登陆页面
打开页面之后,前端脚本会完成下面几个过程:
(1)请求二维码
浏览器打开页面之后,会首先向服务器发送一个请求,获得二维码,
利用解析二维码工具可以得到二维码的内容https://login.weixin.qq.com/l/Ie00Yc04-A==
,可以看出来,实际上这个二维码包含的信息实际上就是这个请求的URL
这个URL后面对应的Ie00Yc04-A==
是一个全局唯一ID,它的用处就是用来识别请求登陆的客户端,如何识别后面会讲解
(2)通过轮询建立『长连接』
打开这个页面之后,浏览器会通过堵塞等待的轮询变相的建立了一个长连接,这个轮询是每间隔25秒向服务器发送一个请求<srcipt>
的GET请求:
如果25秒之内用户没有扫描这个二维码,这个请求会返回200
,结束它的使命。浏览器会再次发送一个请求。这个请求的地址是https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=Ie00Yc04-A==&tip=0&r=-1708735754&_=1577961552943
可以看到,通过这个请求里包含了一个uuid
字段,值就是前面提到的全局唯一ID,我们可以认为这个ID就是这个所谓的长连接的ID
查看了一下微信网页版的前端代码:
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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
| function checkLoginHandler(data) {
switch (data.code) { case 200: loginFactory.newLoginPage(data.redirect_uri).then(function(msg) { var ret = msg.match(/<ret>(.*)<\/ret>/), code = msg.match(/<script>(.*)<\/script>/), skey = msg.match(/<skey>(.*)<\/skey>/), wxsid = msg.match(/<wxsid>(.*)<\/wxsid>/), wxuin = msg.match(/<wxuin>(.*)<\/wxuin>/), passticket = msg.match(/<pass_ticket>(.*)<\/pass_ticket>/), message = msg.match(/<message>(.*)<\/message>/), redirecturl = msg.match(/<redirecturl>(.*)<\/redirecturl>/); if (redirecturl) { window.location.href = redirecturl[1]; return; } if (ret && (ret[1] != '0')) { alert((message && message[1]) || '登陆失败'); monitorService.report(monitorService.AUTH_FAIL_COUNT, 1); location.reload(); return; } $scope.$emit('newLoginPage', { Ret: ret && ret[1], SKey: skey && skey[1], Sid: wxsid && wxsid[1], Uin: wxuin && wxuin[1], Passticket: passticket && passticket[1], Code: code }); if (!utilFactory.getCookie('webwx_data_ticket')) { reportService.report(reportService.ReportType.cookieError, { text: 'webwx_data_ticket 票据丢失', cookie: document.cookie }); } }); break; case 201: $scope.isScan = true; reportService.report(reportService.ReportType.timing, { timing: { scan: Date.now() } }); loginFactory.checkLogin($scope.uuid).then(checkLoginHandler, function(data) { if (!data && window.checkLoginPromise) { $scope.isBrokenNetwork = true; } }); break; case 408: loginFactory.checkLogin($scope.uuid).then(checkLoginHandler, function(data) { if (!data && window.checkLoginPromise) { $scope.isBrokenNetwork = true; } }); break; case 400: case 500: case 0: var refreshTimes = utilFactory.getCookie('refreshTimes') || 0; if (refreshTimes < 5) { refreshTimes++; utilFactory.setCookie('refreshTimes', refreshTimes, 0.5); document.location.reload(); } else { $scope.isNeedRefresh = true; } break; case 202: $scope.isScan = false; $scope.isAssociationLogin = false; utilFactory.setCookie('login_frequency', 0, 2); if (window.checkLoginPromise) { window.checkLoginPromise.abort(); window.checkLoginPromise = null; } doQrcodeLogin(); break; default: } $scope.code = data.code; $scope.userAvatar = data.userAvatar; utilFactory.log('get code', data.code); }
checkLogin: function(uuid, tip) { var deferred = $q.defer(), tip = tip || 0; window.code = 0; window.checkLoginPromise = $.ajax({ url: confFactory.API_login + '?loginicon=true&uuid=' + uuid + '&tip=' + tip + '&r=' + ~new Date(), dataType: "script", timeout: 35000 }).done(function() { var reg = new RegExp('\/' + location.host + '\/') if (window.redirect_uri && window.redirect_uri.indexOf('/' + location.host + '/') < 0) { location.href = window.redirect_uri; return; } var data = { code: window.code, redirect_uri: window.redirect_uri, userAvatar: window.userAvatar }; deferred.resolve(data); }).fail(function() { deferred.reject(); console.log('checkLogin fail.....'); }); return deferred.promise; },
|
关键的代码是上面两个函数,
这样当超过25s后,服务端对请求脚本的代码返回了200
,但是返回的脚本的内容是什么呢?
1 2 3 4 5
| { code: 408 redirect_uri: undefined userAvatar: undefined }
|
其中的code
是408
,然后递归调用checkLogin
,可以看出来,25秒的间隔是由服务端确定的
这样在当用户登录后,微信服务器就可以将下一步的动作作为脚本返回给前端?那为什么不直接写在前端脚本中呢?
如果用户长时间没有操作,页面会自动刷新,重新执行上面的过程,保证了二维码不会因为太久没有扫描而过期
(3)其他操作
除此之外,在打开这个页面后,浏览器还会请求很多其他的资源,比如登陆的默认头像、雪碧图等等,这样当用户通过长连接后进行登陆时就不必再去请求这些资源,可以获得立刻反馈的用户体验
手机扫描二维码
当手机微信扫描这个二维码时,相当于用微信客户端,携带用户的用户名等信息,去访问了二维码对应的连接``https://login.weixin.qq.com/l/Ie00Yc04-A==`,这时候,微信登陆服务器获得了两个信息:
- 唯一ID
Ie00Yc04-A==
,通过这个ID就可以找到上一步建立的长连接信息,也就找到了要登录的设备
- 用户的微信信息,通过这些信息也就找到了要登录的是哪个微信用户
扫描二维码后,客户端就可以通过长连接立刻获得返回的信息:
这时候,长连接返回的信息是:
1 2 3 4 5
| { code: 201 redirect_uri: undefined userAvatar: "data:img/jpg;base64,/9j/4AAQS..." }
|
根据上面的代码返回了201
,这个时候客户端会进行一些信息的上班,然后继续轮询,等待确认登陆
手机确认登陆
用户在客户端点击确认后,相当于向服务器发送了确认登陆的请求,服务器立刻通过轮询长连接返回信息:
1 2 3 4 5
| { "code": 200, "redirect_uri": "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=dsfdfsdf@qrticket_0&uuid=IePV21233ddajSA==&lang=zh_CN&scan=15772296510224", "userAvatar": "data:img/jpg;base64,/9j/4AAQSkZJRgA..." }
|
这时,浏览器根据code
是200
执行响应的代码,通过执行loginFactory.newLoginPage
来访问上面返回的redirect_uri
,根据不同的信息进行登陆或者禁止登陆的操作
以我这次登陆请求为例,不知道什么禁止我使用微信网页版,所以返回来下面的信息:
1
| <error><ret>1203</ret><message>为了你的帐号安全,此微信号不能登录网页微信。你可以使用Windows微信或Mac微信在电脑端登录。Windows微信下载地址:https://pc.weixin.qq.com Mac微信下载地址:https://mac.weixin.qq.com</message></error>
|
如果是正常的话,就会跳转到对应的页面,开始使用微信网页版,扫码登陆的过程就到此结束。
登陆流程图
以前在头条面试的时候被问过扫码登陆的原理,并没有答的很好,面试结束后大概在网上查了查,学习的简书的这篇文章,它总结的基本流程是没有错,如下图:
但是一些具体的技术细节并没有详细介绍,我发现自己并没有搞清楚,于是花了一会时间,又尝试的去学习了一下,还是亲自动手能够搞得更清楚。
疑惑
我还是有一个疑惑:为什么长连接的请求类型是script
呢?
在jQuery的文档里查到的,当dataType
设为script
时的作用是:
把响应的结果当作JavaScript执行。并将其当作纯文本返回。默认情况下不会通过在URL中附加查询字符串变量_=[TIMESTAMP]
进行自动缓存结果,除非设置了cache
参数为true
。Note: 在远程请求时(不在同一个域下),所有POST请求都将转为GET请求。(因为将使用DOM的script
标签来加载)
但是好像在这里没有用到上面的任何一点,只是根据返回的对象的各个属性进行了下一步操作。那直接发送的dataType
为json
格式的请求效果不是一样吗?不解。
参考