使用springboot+angular實現web端微信掃碼登陸

郝澤龍_HZ發表於2023-02-15

概述

現在微信的使用使用者越來越多,如果網站新增上微信登入,就能節省很多使用者註冊時間,極大縮小了註冊流程。會讓使用者覺得特別方便。接下來我們就說一下怎麼來實現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的倉庫地址,請點選

相關文章