网络基础12 二维码扫码登录原理

扫码登陆原理

微信网页版为例,看一下二维码登陆的原理。

浏览器打开登陆页面

打开页面之后,前端脚本会完成下面几个过程:

(1)请求二维码

浏览器打开页面之后,会首先向服务器发送一个请求,获得二维码,

[外链图片转存中…(img-o9zYaSOy-1577966200637)]

利用解析二维码工具可以得到二维码的内容https://login.weixin.qq.com/l/Ie00Yc04-A==,可以看出来,实际上这个二维码包含的信息实际上就是这个请求的URL

这个URL后面对应的Ie00Yc04-A==是一个全局唯一ID,它的用处就是用来识别请求登陆的客户端,如何识别后面会讲解

(2)通过轮询建立『长连接』

打开这个页面之后,浏览器会通过堵塞等待的轮询变相的建立了一个长连接,这个轮询是每间隔25秒向服务器发送一个请求<srcipt>的GET请求:

[外链图片转存中…(img-0hTjiow9-1577966200639)]

如果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

查看了一下微信网页版的前端代码:

function checkLoginHandler(data) {
  /**
   * code:
   *      200: 成功
   *      201:扫描成功,但未点确认
   *      408:未扫描
   *      400:未知
   *      500: login poll srv exception
   *
   */
  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: // 点击取消
      // 1. 关联登录,等待确认,取消
      // 2. 扫码之后,等待确认,取消
      $scope.isScan = false;
      $scope.isAssociationLogin = false;
      utilFactory.setCookie('login_frequency', 0, 2);
      // 终止轮询
      if (window.checkLoginPromise) {
        window.checkLoginPromise.abort();
        window.checkLoginPromise = null;
      }
      doQrcodeLogin();
      break;
    default:
      //todo
  }
  $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;
  // ie8
  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,但是返回的脚本的内容是什么呢?

{
  code: 408
  redirect_uri: undefined
  userAvatar: undefined
}

其中的code408,然后递归调用checkLogin,可以看出来,25秒的间隔是由服务端确定的

这样在当用户登录后,微信服务器就可以将下一步的动作作为脚本返回给前端?那为什么不直接写在前端脚本中呢?

如果用户长时间没有操作,页面会自动刷新,重新执行上面的过程,保证了二维码不会因为太久没有扫描而过期

(3)其他操作

除此之外,在打开这个页面后,浏览器还会请求很多其他的资源,比如登陆的默认头像、雪碧图等等,这样当用户通过长连接后进行登陆时就不必再去请求这些资源,可以获得立刻反馈的用户体验

手机扫描二维码

当手机微信扫描这个二维码时,相当于用微信客户端,携带用户的用户名等信息,去访问了二维码对应的连接``https://login.weixin.qq.com/l/Ie00Yc04-A==`,这时候,微信登陆服务器获得了两个信息:

  1. 唯一IDIe00Yc04-A==,通过这个ID就可以找到上一步建立的长连接信息,也就找到了要登录的设备
  2. 用户的微信信息,通过这些信息也就找到了要登录的是哪个微信用户

扫描二维码后,客户端就可以通过长连接立刻获得返回的信息:

这时候,长连接返回的信息是:

{
  code: 201
  redirect_uri: undefined
  userAvatar: "data:img/jpg;base64,/9j/4AAQS..." // 一个 Base64 格式的头像图片,我进行了截取
}

根据上面的代码返回了201,这个时候客户端会进行一些信息的上班,然后继续轮询,等待确认登陆

手机确认登陆

用户在客户端点击确认后,相当于向服务器发送了确认登陆的请求,服务器立刻通过轮询长连接返回信息:

{
  "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..."
}

这时,浏览器根据code200执行响应的代码,通过执行loginFactory.newLoginPage来访问上面返回的redirect_uri,根据不同的信息进行登陆或者禁止登陆的操作

以我这次登陆请求为例,不知道什么禁止我使用微信网页版,所以返回来下面的信息:

<error><ret>1203</ret><message>为了你的帐号安全,此微信号不能登录网页微信。你可以使用Windows微信或Mac微信在电脑端登录。Windows微信下载地址:https://pc.weixin.qq.com  Mac微信下载地址:https://mac.weixin.qq.com</message></error>

[外链图片转存中…(img-TXpNVBhO-1577966200642)]

如果是正常的话,就会跳转到对应的页面,开始使用微信网页版,扫码登陆的过程就到此结束。

登陆流程图

以前在头条面试的时候被问过扫码登陆的原理,并没有答的很好,面试结束后大概在网上查了查,学习的简书的这篇文章,它总结的基本流程是没有错,如下图:

[外链图片转存中…(img-VmUGgH2c-1577966200643)]

但是一些具体的技术细节并没有详细介绍,我发现自己并没有搞清楚,于是花了一会时间,又尝试的去学习了一下,还是亲自动手能够搞得更清楚。

疑惑

我还是有一个疑惑:为什么长连接的请求类型是script呢?

在jQuery的文档里查到的,当dataType设为script时的作用是:

把响应的结果当作JavaScript执行。并将其当作纯文本返回。默认情况下不会通过在URL中附加查询字符串变量_=[TIMESTAMP]进行自动缓存结果,除非设置了cache参数为true。Note: 在远程请求时(不在同一个域下),所有POST请求都将转为GET请求。(因为将使用DOM的script标签来加载)

但是好像在这里没有用到上面的任何一点,只是根据返回的对象的各个属性进行了下一步操作。那直接发送的dataTypejson格式的请求效果不是一样吗?不解。

参考

发布了382 篇原创文章 · 获赞 91 · 访问量 33万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览