技术派微信公众号自动登录

技术派项目的整个登录流程是基于微信公众号来实现的,整套登录流程的设计?自我实现这样一个流程可以怎样去做?

接下来将以一个工作中的一个功能相对完成的需求作为驱动,来设计确立实时方案来介绍。

方案设计

通常在产品的需求底层、评审之后,就到研发人员出方案设计,常见的方案设计有以下板块。

1、需求相关理解及目标

2、研发的设计方案

  • 相对完整的设计方案
  • 前后端交互方式,接口API约定
  • 测试要点

3、排期

4、验收标准

5、上线计划

这里主要放在前两点

1、背景于目标

技术派是一个文章分享论坛,登录当然是基本的功能,后续操作都需要登录,发文、点赞、评论等。在这里我们主要目的是实现技术派的用户登录

2、设计方案

基于登陆这个需求场景,常用的登录方式有经典的用户名+密码的方式,近年来普及的手机号+验证码和扫码登录也比较流行。

这里给出不同的登录方式实现方案。

2.1用户名密码的方式登录

对于用户名密码方式登录,属于经典的实现方式,一般来讲,使用这种方式来时,除开基础的登陆之外,还需要搭配的用户注册、忘记密码、修改密码啊功能点等

上图分别给出,注册、登录、忘记密码重置流程示意图

基于这种方案,我们的用户表要考虑如下几个

关键信息

  • userName:用于登录的用户名
  • password:登录密码,注意db中不直接存储源码,常见的方案是将用户上传的密码加盐之后计算MD5保存。
  • email/phone:主要用于忘记密码时,向用户发送修改的验证码or重置密码时的url,主要确定这个账号真时xxx在操作。

重点关注

  • 密码注意不要存储明文
  • 忘记密码时,需要给用户发送验证码

优点

用户注册成本低

流程清晰简单、容易理解

缺点

接口多,流程多(除登录还有注册、忘记密码、修改密码等),实现工作量相对较大

用户容易忘记密码,安全性没有其他高

手机号发送验证码时要花钱,邮箱发送验证码时容易被当作垃圾邮件拉黑

2.2验证码登录

验证码登陆的方式对用户而攀体验比较友好,也不用记住密码,这里验证码登录时手机号验证码登录,

流程如下;

从上面的流程示意图可以看出,用户表中核心存储手机号/邮箱即可

phone:采用手机号验证码的方式,村手机号即可

email:采用邮箱接收验证码的方式,存邮箱即可

挂件的动作有两步

1.首先输入手机号/邮箱,然后技术派发送验证码

2.登录:提交手机号/邮箱+验证码

优点

于用户而言操作比较简单,不用做密码相关的操作。

缺点

整个登录流程是分段的,当接收验证码时较慢,可能会阻塞较长时间

同样手机号接收验证码时费钱;邮箱接收验证码时用户体验不太好。

2.3扫码登录

关于扫码登录,对于pc站而言,安装对应的app,这个时标配。

基本流程

一般的扫码登录,前提要求是你已经是网站的用户了,安装对应的app且登录之后,给pc站点的登录新增一个免密的选择方式而已;和技术派场景有出入。

基于上面的操作示意图,核心关键点在于借助App的扫码操作,来识别用户的身份

优点

登录方式简单,成本低

缺点

要求用户下载app

实现先对于上面两个会有难度

2.4基于微信公号登录

本项目无app,采用的是扫码登录的变种,借助微信公众号来做。

虽然我这里登录时展示的是一个二维码,但实际上的操作是借助这个展示的过程,和前端构建一个半长链接,用于向公众号发送验证码之后,微信公众平台会将用户发送信息转发给技术派服务器,通过验证码来识别请求登录的用户身份,找到对应的半长链接,实现用户的自动登录跳转。

基于上面的方案,我们的用户表需要存储一个核心用户标识

uuid:微信公众平台返回的用于唯一标识

优点

对于用户而言登录方式简单,不用记忆密码,有微信号就行

缺点:

实现相对比较复杂

个人公众号不支持自定义二维码参数,因此还需要输入验证码这一步骤,操作相对麻烦一点,企业公众号可以扫码直接登录。

3、方案确定

最终我们选择基于微信公众号来登录

需要准备一个微信公众号,还有一个微信公众平台可以回调的服务器

实现策略

1.微信公众平台配置

我们实际使用的微信公众平台功能较少,主要就是一个接收用户发送的信息,所以需要的配置不多

直接登录后台,开启服务器相关配置

注意,微信接入平台接入时,需要进行一个token验证,即返回它传参的echostr对应的实现也比较简单

    /**
     * 微信的公众号接入 token 验证,即返回echostr的参数值
     *
     * @param request
     * @return
     */
    @GetMapping(path = "callback")
    public String check(HttpServletRequest request) {
        String echoStr = request.getParameter("echostr");
        if (StringUtils.isNoneEmpty(echoStr)) {
            return echoStr;
        }
        return "";
    }

除此之外,另外已给需要实现的就是接收微信公众平台的回调,注意微信公众号采用的是xml进行通讯。

我们需要实现的接口如下

    /**
     * fixme: 需要做防刷校验
     * 微信的响应返回
     * 本地测试访问: curl -X POST 'http://localhost:8080/wx/callback' -H 'content-type:application/xml' -d '<xml><URL><![CDATA[https://hhui.top]]></URL><ToUserName><![CDATA[一灰灰blog]]></ToUserName><FromUserName><![CDATA[demoUser1234]]></FromUserName><CreateTime>1655700579</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[login]]></Content><MsgId>11111111</MsgId></xml>' -i
     *
     * @param msg
     * @return
     */
    @PostMapping(path = "callback",
            consumes = {"application/xml", "text/xml"},
            produces = "application/xml;charset=utf-8")
    public BaseWxMsgResVo callBack(@RequestBody WxTxtMsgReqVo msg) {

2.用户扫码登录

在前面的设计方案中,有一点没特别标注出来,就是用户点击登录之后,弹出一个微信公众号的二维码的同时,我们需要建立一个于前端的半长连接,主要就是用于后续的自动登录跳转

这里我们设计了两个接口,一个是获取登录的验证码,一个是建立半长连接

验证码获取

    /**
     * 获取登录的验证码
     *
     * @return
     */
    @GetMapping(path = "/login/code")
    public ResVo<QrLoginVo> qrLogin(HttpServletRequest request, HttpServletResponse response) {
        QrLoginVo vo = new QrLoginVo();
        vo.setCode(qrLoginHelper.genVerifyCode(request, response));
        return ResVo.ok(vo);
    }





    /**
     * 加一层设备id,主要目的就是为了避免不断刷新页面时,不断的往 verifyCodeCache 中塞入新的kv对
     * 其次就是确保五分钟内,不管刷新多少次,验证码都一样
     *
     * 解释:
     *
     * 该方法接收HTTP请求和响应对象作为参数,用于处理生成验证码并建立与设备ID和缓存相关的逻辑。
     *
     * initDeviceId(request, response) 函数用于初始化设备ID,该ID标识不同的设备。设备ID的生成方式在此处未给出,但可以假设它根据请求的一些信息计算得出。
     *
     * deviceCodeCache 和 verifyCodeCache 是缓存对象,可能是类似于 Guava 的缓存工具。deviceCodeCache 用于存储设备ID与生成的验证码之间的映射关系,而 verifyCodeCache 用于存储验证码与SseEmitter之间的映射关系。
     *
     * 代码从 deviceCodeCache 中获取与设备ID相关的验证码,如果没有找到则会生成新的验证码。
     *
     * 通过从 verifyCodeCache 中获取与该验证码相关的SseEmitter,可以判断是否已经建立了连接。
     *
     * 如果之前存在与该验证码关联的SseEmitter,意味着设备已经建立了连接,需要关闭旧连接以及从缓存中移除旧条目,以便后续重新建立连接。
     *
     * 最后,方法返回生成的验证码。
     *
     * 总之,这个方法的主要目的是生成验证码并管理设备ID以及缓存的关联。它确保了同一设备在五分钟内刷新页面时可以获得相同的验证码,同时在设备建立连接时会处理旧连接的关闭和缓存的更新。
     *
     * @param request  HTTP请求对象,用于获取请求信息。
     * @param response HTTP响应对象,用于处理响应。
     * @return 生成的验证码。
     */
    public String genVerifyCode(HttpServletRequest request, HttpServletResponse response) {
        // 初始化设备ID,该ID用于标识不同的设备。
        String deviceId = initDeviceId(request, response);
        // 从设备验证码缓存中获取之前生成的验证码(如果有)。
        String code = deviceCodeCache.getUnchecked(deviceId);
        // 从验证代码缓存中获取之前与该验证码关联的SseEmitter(服务器发送事件)。
        SseEmitter lastSse = verifyCodeCache.getIfPresent(code);
        if (lastSse != null) {
            // 这个设备之前已经建立了连接,则移除旧的,重新再建立一个; 通常是不断刷新登录页面,会出现这个场景
             关闭旧连接,通常用于处理不断刷新登录页面等情况。
            lastSse.complete();
             从验证代码缓存中移除之前的缓存条目,以便重新建立连接。
            verifyCodeCache.invalidate(code);
        }
        // 返回生成的验证码。
        return code;
    }

关于验证码的获取,做了一个兼容策略,同一个设备,不论访问多少次验证码都是同一个(刷新除外),所以我们做了两个缓存

deviceCodeCache:缓存deviceld设备与验证码之间的映射关系
verifyCodeCache:缓存验证码与半长连接之间的映射关系

半长连接建立

    /**
     * 客户端与后端建立扫描二维码的长连接
     *
     * @param id
     * @return
     */
    @GetMapping(path = "subscribe", produces = {org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE})
    public SseEmitter subscribe(String id) throws IOException {
        return qrLoginHelper.subscribe(id);
    }


   /**
     * 保持与前端的长连接
     * <p>
     * 直接根据设备拿之前初始化的验证码,不直接使用传过来的code
     *
     * @param code
     * @return
     */
    public SseEmitter subscribe(String code) throws IOException {
        HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        HttpServletResponse res = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        String device = initDeviceId(req, res);
        String reaCode = deviceCodeCache.getUnchecked(device);

        // fixme 设置15min的超时时间, 超时时间一旦设置不能修改;因此导致刷新验证码并不会增加连接的有效期
        SseEmitter sseEmitter = new SseEmitter(15 * 60 * 1000L);
        verifyCodeCache.put(code, sseEmitter);
        sseEmitter.onTimeout(() -> verifyCodeCache.invalidate(reaCode));
        sseEmitter.onError((e) -> verifyCodeCache.invalidate(reaCode));
        if (!Objects.equals(reaCode, code)) {
            // 若实际的验证码与前端显示的不同,则通知前端更新
            sseEmitter.send("initCode!");
            sseEmitter.send("init#" + reaCode);
        }
        return sseEmitter;
    }

上面就是一个简单的半长连接建立的过程;并会保存一个验证码与半场链接sseEmitter之间的映射关系;后续在登陆时,就可以通过验证码找到对应的SseEmitter,从而实现登录。

说明

当前上面的两个接口是搭配使用的

  • 前端首先调用获取验证码的接口->这里将设备与验证码建立映射,并会释放之前建立的半长连接,返回验证码

  • 基于验证码来建立半长连接-> 此时构建了验证码与半长连接的映射,因此后续登录时,直接可以通过验证码查到对应的连接客户端,从而实现自动登录。

那么上面这个设计为什么要拆分为两个接口?

这个由于历史原因,最开始的微信公众号登录采用的方案是用户关注公众号之后,输入关键字‘验证码/login’ ,然后技术派返回验证码给公众号,然后用户在登录得页面上输入这个验证码来实现登录的;

鉴于上面的操作流程比较繁琐,所以改成了现在这种操作方式;但是在实现上就没有重新设计,而是直接服用之前的方案。

3.前端调用姿势

上面两个接口主要是后端的接口设计,整个流程还缺少前端的支持,来看一下前端是如何实现的

核心实现如下

      $('#loginModal').on('show.bs.modal', function () {
        console.log("登录弹窗已展示!");
        //这个就是点击技术派的登录按钮,显示二维码弹出时触发的逻辑
        loginCode();
      })
      $('#refreshCode').click(() => {
          refreshCode()
      })

 function loginCode() {
        $.ajax({
          url: "/login/code", dataType: "json", type: "get", success: function (data) {
            const code = data['result']['code'];
            //首先请求验证码,然后基于验证码建立半长连接
            buildConnect(code);
            if ([[${!#strings.equals(global.env, 'prod')}]]) {
              document.getElementById('mockLogin').setAttribute('data-verify-code', code);
              document.getElementById('mockLogin2').setAttribute('data-verify-code', code);
            }
          }
        })
      }

      /**
       * 建立半长连接,用于实现自动登录
       * @param code
       */
 function buildConnect(code) {
          const stateTag = document.getElementById('state');
          const codeTag = document.getElementById('code');
          const subscribeUrl = "/subscribe?id=" + code;
          const source = new EventSource(subscribeUrl);
          source.onmessage = function (event) {
              let text = event.data;
              console.log("receive: " + text);
              if (text.startsWith('refresh#')) {
                  // 刷新验证码
                  const newCode = text.substring(8).trim();
                  codeTag.innerText = newCode;
                  stateTag.innerText = '已刷新';
                  stateTag.style.display = 'block';
                  if ([[${!#strings.equals(global.env, 'prod')}]]) {
                      document.getElementById("mockLogin").setAttribute('data-verify-code', newCode);
                      document.getElementById("mockLogin2").setAttribute('data-verify-code', newCode);
                  }
              } else if (text === 'scan') {
                  // 二维码扫描
                  stateTag.innerText = '已扫描';
                  stateTag.style.display = 'block';
              } else if (text.startsWith('login#')) {
                  // 登录格式为 login#cookie
                  if(autoRefresh) {
                    window.clearInterval(autoRefresh);
                  }
                  console.log("登录成功,保存cookie", text)
                  document.cookie = text.substring(6);
                  source.close();
                  if (window.location.pathname === "/login") {
                      // 登录成功,跳转首页
                      window.location.href = "/";
                  } else {
                      // 刷新当前页面
                      window.location.reload();
                  }
              } else if (text.startsWith("init#")) {
                const newCode = text.substring(5).trim();
                codeTag.innerText = newCode;
                console.log("初始化验证码: ", newCode);
              }
          };

          source.onopen = function (evt) {
              console.log("开始订阅");
          }
          source.onerror = function (evt) {
            console.log("连接错误,重新开始", evt)
            buildConnect(code);
          }
          codeTag.innerText = code;
          stateTag.innerText = '验证码有效期为五分钟,若过期后可刷新验证码';

          if(autoRefresh) {
            window.clearInterval(autoRefresh);
          }
          // 先关闭自动刷新验证码
          // autoRefresh = setInterval(function () {
          //     refreshCode();
          // }, 5 * 60 * 1000)
      }

上面是前端js实现的,整个逻辑与后端接口的设计是搭配的。先获取验证码再建立连接。

4.微信公众号回调实现自动登录

上面的两步操作之后,技术派的前端用户操作与后台的基本逻辑就算完成了;

  • 用户登录之后 ->与后端建立半长连接

接下来就是用户将验证码发送给公众号,然后公众号将用户输入转发给技术派后端注册的回调接口了。

回调接口如上,因为我们的公众号为个人公众号,所以图中的if逻辑我们走不到,重点查看下面的

WxHelper.buildResponseBody登录逻辑如下,其他的是自动回复内容,不用关心

上面区分了两步

1.用户注册,并生成用于身份识别的sessionId

2.找到对应的长连接,自动登录跳转

说明

上面的这套具体实现以实现的源码为准。

5.小结

基于上面这个设计思路以及关键的实现动作,整个基于微信公众号的登录方案就实现了。

  • 33
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《大数据技术及架构图解实战》是一本介绍大数据技术和架构的实用指南。该书从理论与实践相结合的角度,系统地介绍了大数据技术的基本概念、发展历程、关键技术及应用案例。 这本书的主要内容包括:大数据技术的基础知识,如数据存储、数据处理和分析等;大数据架构的设计原则和实践方法,包括数据仓库架构、流式处理架构等;大数据应用的具体案例,如金融、电商、物流等行业中的应用实践。 在阅读《大数据技术及架构图解实战》这本书之后,读者可以掌握大数据技术的基本概念和原理,了解大数据技术的发展趋势和应用场景,掌握大数据架构的设计原则和方法,具备进行大数据项目开发和架构设计的能力。 此外,该书还提供了大量的图解和实例,帮助读者更加直观地理解大数据技术和架构。通过理论与实践相结合的方式,读者可以更好地理解和应用大数据技术,为企业提供更加精准的数据分析和决策支持,从而提升企业的竞争力和创新能力。 总之,对于想要了解和应用大数据技术的读者来说,《大数据技术及架构图解实战》是一本不可或缺的参考书。它系统地介绍了大数据技术的基本概念和原理,提供了丰富的实例和图解,帮助读者更好地理解和应用大数据技术。无论是从事大数据项目开发的技术人员还是对大数据技术感兴趣的读者,都能从这本书中获得很多有价值的知识和经验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值