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

引言

现实中经常会需要我们需要扫码授权登陆,有的时候是借助微信授权登陆,有的时候商户需要登陆某个特定的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或者扫码识别等都是通过服务器来校验的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 前言 在网上看别人的面经的时候,发现一个有意思并且自己从来没有思考过的问题:生活中,我们登陆淘宝或者微信时又是可以...
    MrYun阅读 7,973评论 1 70
  • 前几天买阿里云服务器的时候,被扫码登陆给吸引到了。然后就一直在琢磨自己的实现方式。也许是巧合吧,昨晚竟然在梦中找到...
    尽情的嘲笑我吧阅读 564评论 0 5
  • 我以勤奋努力给自己更广阔的人生。 我决定勤奋到让自己害怕,还邀请我的身体来支持我的野心。 我对世界的好奇驱使着我去...
    辉子时间阅读 247评论 0 0
  • 再一次梦见考试,神经有点紧绷。 开头有点模糊了,只能记得,我要穿着一件胸前有缀着几颗白球的白色亚麻长袖去完成什么任...
    阿阿阿切诶阅读 244评论 0 0
  • 民以食为天。感恩单位最近给职工调换了更好的就餐地点和服务,化零为整,整合了各实体单位零散各处的小食堂。方便了大...
    仰望星空a阅读 123评论 0 0