美文网首页后端之美-ASP.netJavaJava后端
网页扫码请求登录的逻辑原理与实现

网页扫码请求登录的逻辑原理与实现

作者: YukunWen | 来源:发表于2018-12-21 16:28 被阅读1261次
    引言

    现实中经常会需要我们需要扫码授权登陆,有的时候是借助微信授权登陆,有的时候商户需要登陆某个特定的app,在该app中扫码登陆。那么我们今天就来分析一下扫码登陆,这背后究竟发生了怎么样的请求交互,以及是怎么实现的。
    下面我们以微信为例,调了微信商户登陆平台这个页面进行分析:
    https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F

    针对微信网页进行分析

    首先如图1,一进入页面之后会请求生成一个二维码。

    图1.初始请求拿到二维码

    针对一个请求,前台会多次有间隔地轮询,如图2,如图3。

    图2.反复请求 图3.请求所带参数

    请求的响应结果有 "wait scan" 和 “二维码过期” 两种情况,如图4,图5所示。

    图4.有效期内请求返回的结果 图5.超过有效期返回的结果

    在二维码过期后,点击刷新二维码,之后便会重新请求获取到二维码,再次的轮询请求后台结果,如图6所示。

    图6.点击二维码进行刷新后的效果

    仿照设计与实现

    设计

    考虑的点:

    1. 二维码生成与展示。

    这里我们采用前端生成随机串,以便前端后期不断的轮询。具体随机串藏在二维码中生成接口可以参考我之前的博文——java生成QR二维码

    1. 轮询间隔,后端对应的过期与超时等返回。

    这里新版的微信登陆采用的是前端sleep,频繁请求后端。在之前没改版的时候采用的是长连接,一次请求由后端自行轮询。本文采用后端轮询的形式。

    1. APP扫码登陆。

    APP扫码识别出了二维码中的随机串,应该告诉服务器验证成功,待web下一次轮询服务器的时候要返回相应的token和登陆成功等其他信息。

    将这几个点结合在一起就有了图7。

    图7.设计逻辑图

    那么经过分析,我们得知后端至少要3个接口。分别生成二维码,给WEB轮询,和给APP请求。生成二维码的参考博主之前的博文,目前就不在这里重复。下面给出其他两个接口的实现。

    实现

    1. 给WEB轮询接口
      采用递归的形式实现轮询。利用redis存储了前端生成的随机串,设置0为默认值
    
    CONNECT_TIME_OUT("连接超时",2001),
      private static String DEAFULT_ID = "0";
     /**
         * 获取token
         *
         * @param webLoginDTO
         * @return
         */
        @PostMapping(value = "webLoginCode/ask")
        @ResponseBody
        public RestResponse<Map<String, Object>> askWebLoginCode(@JsonParam WebLoginDTO webLoginDTO) {
            String webLoginCode = webLoginDTO.getWebLoginCode();
            Assert.notNull(webLoginCode, "传入的随机串为空");
            return RestResponse.ok(askWebLoginCodePolling(webLoginCode, MAX_RETRY));
        }
    
        /**
         * 轮询获取token
         *
         * @param webLoginCode
         * @param retry
         * @return
         */
        @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
        public Map<String, Object> askWebLoginCodePolling(String webLoginCode, int retry) {
            if (retry == 0) {
                throw new FantuanRuntimeException(FantuanErrors.CONNECT_TIME_OUT.getMessage(),
                        FantuanErrors.CONNECT_TIME_OUT.getCode());
            }
            Map<String, Object> resultMaps = new HashMap<>();
            String varR = varPool.getVar(getWebLoginVarPool(webLoginCode));
            if (StringUtils.isBlank(varR)) {
                throw new FantuanRuntimeException("已经过期,请重新刷新二维码");
            }
    
            if (!DEAFULT_ID.equals(varR)) {
                Map<String, Object> maps = JsonUtil.stringToMap(varR);
                String varResult = MapUtils.getString(maps, "uid");
                User user = userService.selectById(Long.valueOf(varResult));
                String webAuthKey = user.getWebAuthKey();
                if (StringUtils.isBlank(webAuthKey)) {
                    webAuthKey = RandomNumberUtil.creatUUID32();
                    user.setWebAuthKey(webAuthKey);
                    userService.updateById(user);
                }
                resultMaps.put("userName", user.getUsername());
                resultMaps.put("uid", varResult);
                resultMaps.put("token", webAuthKey);
                resultMaps.put("extraInfo", MapUtils.getObject(maps, "extraInfo"));
                return resultMaps;
            } else {
                //这里需要hold住链接
                try {
                    Thread.sleep(1000);
                    return askWebLoginCodePolling(webLoginCode, retry - 1);
                } catch (InterruptedException e) {
                    log.error("", e);
                }
            }
            return resultMaps;
        }
    

    2.给APP请求接口
    APP扫码登陆后,会把一些有用的信息给传递过来。这里后端做成了一个map extraInfo去接收,到时候整个extraInfo会返回给WEB端。
    这样子的好处,后端就是成了一个验证平台而已,需要的信息只要由APP和WEB端定义好即可。

     /**
         * app扫码登陆
         *
         * @param webLoginDTO
         */
        @PostMapping(value = "webLoginCode/check")
        @ResponseBody
        public RestResponse<String> checkWebLoginCode(@JsonParam WebLoginDTO webLoginDTO) {
            Long uid = SecurityUtils.getLoginAccountId();
            if (uid <= 0) {
                throw new FantuanRuntimeException("请登陆");
            }
    
            Assert.notNull(webLoginDTO, "传入的对象不能为空");
            String webLoginCode = webLoginDTO.getWebLoginCode();
            Assert.notNull(webLoginCode, "传入的二维串随机码不能为空");
            Map<String, String> result = new HashMap<>();
            if (StringUtils.isBlank(varPool.getVar(getWebLoginVarPool(webLoginCode)))){
                throw new FantuanRuntimeException("该二维码已经过期");
            }
            Map<String, Object> maps = new HashMap<>();
            maps.put("uid", uid);
            maps.put("extraInfo", webLoginDTO.getExtraInfo());
            varPool.setVar(getWebLoginVarPool(webLoginCode), JsonUtil.mapToJson(maps), 60, TimeUnit.SECONDS);
            return RestResponse.ok("执行完成了");
        }
    
    1. 另外给出部分的WEB端代码
    <template>
      <div class="page">
        <top-nav :buttons="false" />
        <div class="page-main">
          <div class="qr-code">
            <img class="qr-code-image" v-if="qrcode" :src="$apiDomain + '/jv/anonymous/login/webLoginCode/' + qrcode" alt="登录二维码" @load="imageLoaded" />
            <div v-if="expired" class="qr-code-expired" @click.stop="refresh">
              <i class="iconfont icon-shuaxin"></i>
              <div class="expired-tip">二维码已失效,请点击刷新</div>
            </div>
          </div>
          <div class="scan-tip">扫描二维码</div>
          <div class="scan-sub-tip">在电脑端进行活动编辑</div>
        </div>
        <us :onlyCopyright="true" />
      </div>
    </template>
    
    <script>
    import TopNav from '@/components/TopNav'
    import Us from '@/components/Us'
    export default {
      data () {
        return {
          qrcode: '',
          expired: false
        }
      },
      components: { TopNav, Us },
      methods: {
        getQrcode (length) {
          this.expired = false
          let rString = ''
          let timeStr = new Date().getTime().toString()
          timeStr = timeStr.substring(timeStr.length - length)
          let rendomStr = this.getRandom(length)
          for (let i = 0; i < length; i++) {
            rString += (rendomStr[i] + timeStr[i])
          }
          return rString
        },
        getRandom (length) {
          if (length > 0) {
            let data = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
            let nums = ''
            for (let i = 0; i < length; i++) {
              let r = parseInt(Math.random() * 61)
              nums += data[r]
            }
            return nums
          } else {
            return false
          }
        },
        getUserInfo (qrcode) {
          if (!qrcode) {
            return false
          }
          sessionStorage.clear()
          let rData = {
            webLoginCode: qrcode
          }
          this.$ajax('/webLoginCode/ask', {data: rData, dontToast: true}).then(res => {
            console.log('userInfo_res', res)
            if (res && res.data && !res.error) { // 获取用户信息成功
              sessionStorage.setItem('token', res.data.token)
              sessionStorage.setItem('userId', res.data.uid)
              sessionStorage.setItem('userName', res.data.userName)
              this.$router.replace({name: 'ActivityEdit'})
            }
          }).catch(err => {
            if (err && err.data && err.data.error) {
              console.log('userInfo_err', err)
              if (err.data.error.toString() === '2001') {
                // 重新获取
                console.log('链接超时')
                this.getUserInfo(qrcode)
              } else {
                console.log('获取信息出错')
                this.expired = true
              }
            }
          })
        },
        refresh () {
          this.qrcode = this.getQrcode(8)
        },
        imageLoaded () {
          console.log('imageLoaded')
          this.getUserInfo(this.qrcode)
        }
      },
      mounted () {
        this.refresh()
      }
    }
    </script>
    

    最终web端的页面如图8所示。这里采用的是后端长连接的形式,所以不像是新版微信那样请求是断续的。当然要做成那样,只要后端的轮询上限改成1,前端加上一个短暂的sleep即可。

    图8.最终结果

    总结:

    其实无论是扫码登陆,还是网页的扫码支付,其实本质上都是藏着一个长连接/长轮询去监听服务器的状态变化。毕竟回call或者扫码识别等都是通过服务器来校验的。

    相关文章

      网友评论

        本文标题:网页扫码请求登录的逻辑原理与实现

        本文链接:https://www.haomeiwen.com/subject/frdmkqtx.html