使用springboot+angular实现web端微信扫码登陆

概述

现在微信的使用用户越来越多,如果网站添加上微信登录,就能节省很多用户注册时间,极大缩小了注册流程。会让用户觉得特别方便。接下来我们就说一下怎么来实现Web端微信扫码登录。

准备工作

1.实现内网穿透,推荐工具:飞鸽快速跳转
实现内网穿透原因:微信无法访问私有ip地址,同时我们在进行测试时,使用的家用的ip大多都是私有ip地址,所以要通过内网穿透来使用公网ip映射到我们的服务。
2.申请微信公众平台测试账号并进行配置点击查看更多

Websocket

实时通信

我们通常有三种方法实现实时通信:
1.ajax轮询
ajax轮询的原理非常简单,让浏览器每隔几秒就像服务器发送一个请求,询问服务器是否有新的信息.
2.http 长轮询
长轮询的机制和ajax轮询差不多,都是采用轮询的方式,不过过去的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起链接后,如果没有消息,就一直不返回response给客户端。直到有新的消息才返回,返回完之后,客户端再此建立连接,周而复始.
3.WebSocket
WebSocketHTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议.在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送,不需要繁琐的询问和等待.

对比:ajax轮询和长轮询都是非常耗费资源的,而WebSocket,只需要经过一次HTTP请求,就可以与服务端进行源源不断的消息收发了.

实现过程

sockjs

SockJS是一个浏览器的JavaScript库,它提供了一个类似于网络的对象,SockJS提供了一个连贯的,跨浏览器的JavaScriptAPI,它在浏览器和Web服务器之间创建了一个低延迟,全双工,跨域通信通道. SockJS提供了浏览器兼容性,优先使用原生的WebSocket,如果某个浏览器不支持WebSocket,SockJS会自动降级为轮询.

STOMP

STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的简单文本协议: WebSocket是一个消息架构,不强制使用任何特定的消息协议,它依赖于应用层解释消息的含义.与HTTP不同,WebSocket是在传输层上进行数据实现和处理的,会将字节流转化为文本/二进制消息,因此,对于实际应用来说,WebSocket的通信形式层级过低,因此,可以在WebSocket之上使用STOMP协议,来为浏览器 和 server间的 通信增加适当的消息语义。

STOMP与WebSocket 的关系:

1.HTTP协议解决了web浏览器发起请求以及web服务器响应请求的细节,假设HTTP协议不存在,只能使用TCP套接字来编写web应用,通信双方在应用层的协议三要素便可能不一致。
2.直接使用WebSocket(SockJS)就很类似于使用TCP套接字来编写web应用,因为没有高层协议,就需要我们定义应用间发送消息的语义,还需要确保连接的两端都能遵循这些语义.
3.同HTTPTCP套接字上添加请求-响应模型层一样,STOMPWebSocket之上提供了一个基于帧的线路格式层,用来定义消息语义.

实战

微信扫码登陆过程:
image.png

1.实现前后台WebSocket连接:

前台首先安装sockjs-clientstompjs:

npm install sockjs-client
npm install stompjs

前台建立websocket连接简单示例:

        const socket = new SockJS('http://localhost:8080/demo-stomp-endpoint');
        const stompClient = Stomp.over(socket);
        stompClient.connect({
          'ws-auth-token': this.uuid
        }, (frame: any) => {
          // 添加个uuid, 用于后续进行debug,看是否为单例.
          stompClient.id = uuid();
          this.stompClientSubject.next(stompClient);
        });

前台注册路由,充当后台主动访问的接口:

/**
   * 注册路由
   * @param router 路由
   * @param subject 后台回发webSocket时发送数据流
   */
  register<T>(router: string, subject: Subject<T>): void {
    if (this.observables[router]) {
      throw new Error('未能够重复注册关键字' + router);
    }
    console.log('register');
    this.stompClient$.pipe(filter(v => v !== null), first()).subscribe(stompClient => {
      stompClient.subscribe(this.getUrl(router), (data: any) => {
        console.log(data);
        subject.next(data);
      });
    });
  }

后台引入WebSocket相关依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

后台引入公众号的相关依赖:

<dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>weixin-java-mp</artifactId>
            <version>4.4.0</version>
</dependency>
<dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>wx-java-mp-spring-boot-starter</artifactId>
            <version>4.4.0</version>
</dependency>

后台定义与前台连接点:

  /**
   * 定义一个连接点(处理第一次webSocket的握手)
   */
  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/demo-stomp-endpoint")
        .setAllowedOriginPatterns("http://localhost:4200")
        .withSockJS();
  }

后台定义出口前缀和入口前缀:

/**
   * 配置消息经纪人
   * 配置一个入口前缀,一个出口前缀。
   * 注意:出口需要保留/user前缀,stomp主动向某个用户发送数据时,将以/user前缀前头(可配置)
   */
  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) {
    // 设置入口前缀,处理所有以app打头的请求
    config.setApplicationDestinationPrefixes("/app");
    // 设置出口前缀,处理所有以/stomp打头的出口数据
    config.enableSimpleBroker("/stomp");
  }

逻辑实现

1.首先在打开页面时,进行路由注册,便于后台主动向前台发起请求

// 注册前台扫码绑定的路由,扫码绑定后,后台主动发起请求
 this.websocketServer.register('/user/stomp/scanBindUserQrCode', this.onScanBindUserQrCode);
// 注册前台扫码登陆的路由,扫码登陆后,后台主动发起请求
 this.websocketServer.register('/user/stomp/scanLoginQrCode', this.onScanLoginQrCode);

随后前台发起获取登陆二维码请求,同时传输唯一标识码,用于扫码成功后后台主动发起登陆成功请求给前台:

/**
   * 获取登录二维码
   */
  getLoginQrCode(): Observable<string> {
    return this.httpClient.get<string>(`${this.baseUrl}/getLoginQrCode/${this.websocketServer.uuid}`);
  }

2.后台响应获取登陆二维码同时对扫码事件进行处理,:

@Override
  public String getLoginQrCode(String wsLoginToken, HttpSession httpSession) {
    try {
      if (this.logger.isDebugEnabled()) {
        this.logger.info("1. 生成用于回调的uuid,请将推送给微信,微信当推送带有UUID的二维码,用户扫码后微信则会把带有uuid的信息回推过来");
      }
      // qrUuid用于换取微信跳转的ticket,以根据ticket换取二维码url
      String qrUuid = UUID.randomUUID().toString();
      WxMpQrCodeTicket wxMpQrCodeTicket = this.weChatMpService.getQrcodeService().qrCodeCreateTmpTicket(qrUuid, 10 * 60);
      // 增加事务处理,对扫描事件等处理进行处理
      this.weChatMpService.addHandler(qrUuid, new WeChatMpEventKeyHandler() {
        long beginTime = System.currentTimeMillis();
        private Logger logger = LoggerFactory.getLogger(this.getClass());

        @Override
        public boolean getExpired() {
          return System.currentTimeMillis() - beginTime > 10 * 60 * 1000;
        }

        /**
         * 扫码后调用该方法
         * @param wxMpXmlMessage 扫码消息
         * @param weChatUser 扫码用户
         * @return 输出消息
         */
        @Override
        public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, WeChatUser weChatUser) {
          if (this.logger.isDebugEnabled()) {
            this.logger.info("2. 用户扫描后触发该方法, 发送扫码成功的同时,将wsUuid与微信用户绑定在一起,用后面使用wsU");
          }
          String openid = wxMpXmlMessage.getFromUser();
          if (openid == null) {
            this.logger.error("openid is null");
          }
          if (weChatUser.getUser() != null) {
            // 此处的随机生成的uuid与用户绑定,并将其返回给前台,用uuid再次登陆
            String uuid = UUID.randomUUID().toString();
            System.out.println("uuid是" + uuid);
            bindWsUuidToWeChatUser(uuid, weChatUser);
           // 此处的wx-auth-token是前台生成的uuid,用于标识唯一前台 
            simpMessagingTemplate.convertAndSendToUser(wsLoginToken,
                    "/stomp/scanLoginQrCode",
                    uuid);
            return new TextBuilder().build(String.format("登录成功,登录的用户为: %s", weChatUser.getUser().getName()),
                    wxMpXmlMessage,
                    null);
          } else {
            // 扫码后发现没有绑定,返回给前台空uuid,同时返回给微信用户未绑定提示
            // 此处的wx-auth-token是前台生成的uuid,用于标识唯一前台
            simpMessagingTemplate.convertAndSendToUser(wx-auth-token,
                    "/stomp/scanLoginQrCode",
                    false);
            return new TextBuilder().build(String.format("登录原则,原因:您尚未绑定微信用户"),
                    wxMpXmlMessage,
                    null);
          }
        }
      });
      return this.weChatMpService.getQrcodeService().qrCodePictureUrl(wxMpQrCodeTicket.getTicket());
    } catch (Exception e) {
      this.logger.error("获取临时公众号图片时发生错误:" + e.getMessage());
    }
    return "";
  }

3.前台接收到uuid后,使用uuid作为用户名和密码进行正常登陆:

this.login({username: uuid, password: uuid});

4.后台在登陆验证方式中增加根据uuid进行验证:

/**
   * 校验微信扫码登录后的认证ID是否有效
   * @param wsAuthUuid websocket认证ID
   */
  @Override
  public boolean checkWeChatLoginUuidIsValid(String wsAuthUuid) {
    return this.map.containsKey(wsAuthUuid);
  }

实现逻辑图如下:
P1S7UG%$I@00J0ONRORJIHO.png

最后是demo的仓库地址,请点击

167 声望
15 粉丝
0 条评论
推荐阅读
Springboot实现文件上传和下载
在一些文件存储量很小的工程中,有一些上传文件放置在工程本身的目录下,但是随着文件上传的量越来越大,工程本身所在的文件夹容量会越来越大,不仅打包和部署的效率会降低,工程的启动和运行也会变慢,所以一般...

郝泽龙_HZ2阅读 879

「多图预警」完美实现一个@功能
一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala...(内心 OS:不听,讲重点)产品大大:咱们的客服 I...

wuwhs40阅读 4.8k评论 5

封面图
涨姿势了,有意思的气泡 Loading 效果
今日,群友提问,如何实现这么一个 Loading 效果:这个确实有点意思,但是这是 CSS 能够完成的?没错,这个效果中的核心气泡效果,其实借助 CSS 中的滤镜,能够比较轻松的实现,就是所需的元素可能多点。参考我们...

chokcoco21阅读 2.2k评论 3

你可能不需要JS!CSS实现一个计时器
CSS现在可不仅仅只是改一个颜色这么简单,还可以做很多交互,比如做一个功能齐全的计时器?样式上并不复杂,主要是几个交互的地方数字时钟的变化开始、暂停操作重置操作如何仅使用 CSS 来实现这样的功能呢?一起...

XboxYan23阅读 1.6k评论 1

封面图
过滤/筛选树节点
又是树,是我跟树杠上了吗?—— 不,是树的问题太多了!🔗 相关文章推荐:使用递归遍历并转换树形数据(以 TypeScript 为例)从列表生成树 (JavaScript/TypeScript) 过滤和筛选是一个意思,都是 filter。对于列表来...

边城18阅读 7.8k评论 3

封面图
Vue2 导出excel
2020-07-15更新 excel导出安装 {代码...} src文件夹下新建一个libs文件夹,新建一个excel.js {代码...} vue页面中使用 {代码...} ===========================以下为早期的文章今天在开发的过程中需要做一个Vue的...

原谅我一生不羁放歌搞文艺14阅读 20k评论 9

【代码鉴赏】简单优雅的JavaScript代码片段(一):异步控制
Promise.race不满足需求,因为如果有一个Promise率先reject,结果Promise也会立即reject;Promise.all也不满足需求,因为它会等待所有Promise,并且要求所有Promise都成功resolve。

csRyan26阅读 3.3k评论 1

167 声望
15 粉丝
宣传栏