第三方登陸:微信掃碼登入

weiewiyi發表於2023-02-07

前言

各種官方網站通常都會有app、微信公眾號等。比如央視網,銀行等。

當我們關注公眾號或者app後,這些應用就可以在移動端方便地將資訊推送給使用者。

統一各產品線的賬號體系,實現一個賬號處處使用的目標是非常有必要的。

於是有需求:可以實現微信掃碼登陸。

微信對接

通常網站與個人微信的對接有兩種方式:

第一種是Oauth2登陸:

使用者透過掃描網站的二維碼, 授權公開資訊,如暱稱、頭像等。便可以成功登陸網站。

例如在愛奇藝網站掃描二維碼登陸後,授權給網站,之後成功登陸愛奇藝。

image.png

登陸完之後,微信會提示資訊。

1675689425421.png

第二種是關注微信公眾號

網站自己彈出一個二維碼,掃描二維碼後彈出公眾號的關注介面、只要一關注公眾號網站自動登入、第二次掃描登入的時候網站直接登入。

例如這種,隨便找的網站
掃碼後跳到公眾號,關注後自動登陸。
image.png


這兩種掃碼登陸方法,對應微信提供的兩種開發方式

一種是基於微信公眾平臺的掃碼登入,另一種是基於微信開放平臺的掃碼登入。

微信開放平臺就是為了讓第三方應用投入微信的懷抱而設計的,這第三方應用指的是比如android、ios、網站、系統等;
引用
微信公眾平臺就是為了讓程式設計師小夥伴利用微信自家技術(公眾號、小程式)開發公眾號、小程式而準備的。

微信開放平臺入口:https://open.weixin.qq.com/

微信公眾平臺入口:https://mp.weixin.qq.com/

image.png

兩者使用微信掃碼登入的區別:

微信開放平臺需要開企業認證才能註冊。比較難申請

微信公眾平臺需要認證微信服務號,才能進行掃碼登入的開發。只需申請一個公眾號。

下面採用第二種。

後臺與公眾號配置

後臺使用的是spring boot。前臺用的angular

這裡引入github一個公眾號開發工具包
github倉庫

後臺pom.xml依賴:

       <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>

1.申請一個公眾號。若無,可用測試公眾號。

微信公眾平臺介面測試帳號申請

2.配置伺服器資訊

URL為開發者伺服器的介面地址,微信伺服器透過該介面與開發者伺服器建立連線。Token可由開發者可以任意填寫,用作生成簽名(該Token會和介面URL中包含的Token進行比對,從而驗證安全性)
若是沒有伺服器,可以用ngfork進行內網穿透,免費好用。這裡用的就是ngfork

image.png

3.配置後臺:

# 公眾號配置(必填)
wx:
  mp:
    appid: wxaf7fe05a8xxxxxxxxx
    secret: 57b48fcec2d5db1axxxxxxxxxxx
    token: yunzhi
    aesKey: 123
@Service
public class WeChatMpService extends WxMpServiceImpl {

  @Autowired
  private WxMpConfig wxMpConfig;

  @PostConstruct
  public void init() {
    final WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
    // 設定微信公眾號的appid
    config.setAppId(this.wxMpConfig.getAppid());
    // 設定微信公眾號的app corpSecret
    config.setSecret(this.wxMpConfig.getAppSecret());
    // 設定微信公眾號的token
    config.setToken(this.wxMpConfig.getToken());
    // 設定訊息加解密金鑰
    config.setAesKey(this.wxMpConfig.getAesKey());
    super.setWxMpConfigStorage(config);
  }
}

4.驗證伺服器地址的有效性

開發者提交資訊後,微信伺服器將傳送GET請求到剛才填寫的伺服器地址URL上,

經過確認後,需要向微信伺服器原樣返回echostr引數內容,接入才生效,否則接入失敗。

例如,微信伺服器未正確收到返回資訊,顯示配置失敗。

image.png


GET請求攜帶引數如下表所示:

引數描述
signature微信加密簽名,signature結合了開發者填寫的 token 引數和請求中的 timestamp 引數、nonce引數
timestamp時間戳
nonce隨機數
echostr隨機字串

驗證簽名的主要程式碼:

用的驗證簽名的函式是weixin-java-mp包提供的。

@RequestMapping("wechat")
@RestController
public class WechatController {
 
  @Autowired
  WeChatMpService weChatMpService;


  /*
   * @param signature 微信加密簽名,signature結合了開發者填寫的 token 引數和請求中的 timestamp 引數、nonce引數。
   * @param timestamp 時間戳
   * @param nonce     這是個隨機數
   * @param echostr   隨機字串,驗證成功後原樣返回
   */
  @GetMapping
  public void get(@RequestParam(required = false) String signature,
                  @RequestParam(required = false) String timestamp,
                  @RequestParam(required = false) String nonce,
                  @RequestParam(required = false) String echostr,
                  HttpServletResponse response) throws IOException {
    if (!this.weChatMpService.checkSignature(timestamp, nonce, signature)) {
      this.logger.warn("接收到了未透過校驗的微信訊息,這可能是token配置錯了,或是接收了非微信官方的請求");
      return;
    }
    response.setCharacterEncoding("UTF-8");
    response.getWriter().write(echostr);
    response.getWriter().flush();
    response.getWriter().close();
  }
}

掃碼登入的開發

先來看一下大致流程
image.png

1.向微信伺服器傳送請求,獲取access_token。

image.png
微信請求access_token文件

access_token是公眾號的全域性唯一介面呼叫憑據,公眾號呼叫各介面時都需使用access_token。開發者需要進行妥善儲存。access_token的儲存至少要保留512個字元空間。access_token的有效期目前為2個小時,需定時重新整理,重複獲取將導致上次獲取的access_token失效。

url(1)請求示例地址: https://api.weixin.qq.com/cgi...

所以假如直接對微信文件開發的話,需要先請求access_token。

但用了weixin-java-mp包,會幫我們做這件事。下面跳到下一步

2.請求獲取ticket,用於換取登陸二維碼。

image.png

微信獲取二維碼文件

獲取帶引數的二維碼的過程包括兩步:

  1. 首先建立二維碼ticket
  2. 憑藉 ticket 到指定 URL 換取二維碼。

目前有2種型別的二維碼:

  • 臨時二維碼,有過期時間的,最長可以設定為在二維碼生成後的30天后過期,能夠生成較多數量。
  • 永久二維碼,無過期時間的,數量較少(目前為最多10萬個)。

臨時二維碼足以滿足需求。

用包提供的方法獲取ticket

String qrUuid = UUID.randomUUID().toString();
WxMpQrCodeTicket wxMpQrCodeTicket = this.wxMpService.getQrcodeService().qrCodeCreateTmpTicket(qrUuid, 10 * 60);

每次建立二維碼 ticket 需要提供一個開發者自行設定的引數(scene_id), 這裡用了隨機生成的uuid。

3.用ticket向微信請求二維碼。前端顯示

image.png

請求二維碼文件

獲取二維碼 ticket 後,開發者可用 ticket 換取二維碼圖片。
HTTP GET請求(請使用 https 協議)https://mp.weixin.qq.com/cgi-...
提醒:TICKET記得進行UrlEncode

ticket正確情況下,http 返回碼是200,是一張圖片的url地址,可以直接展示或者下載。

使用包提供的方法進行請求獲取

String qrCodeUrl = this.wxMpService.getQrcodeService().qrCodePictureUrl(wxMpQrCodeTicket.getTicket());

之後返回給前端,前端用<img>標籤處理,作為src的值。

<img class="img-thumbnail" [src]="qrCodeUrl"/>

4.使用者掃描二維碼,微信進行事件推送

image.png

微信使用者使用公共號時可能會產生很多事件,例如

  • 關注/取消關注事件
  • 掃描帶引數二維碼事件
  • 自定義選單事件
  • 點選選單拉取訊息時的事件推送
  • 點選選單跳轉連結時的事件推送

而當前的場景就是使用者掃描帶引數二維碼事件

使用者掃描帶場景值二維碼時,可能推送以下兩種事件:

  • 如果使用者還未關注公眾號,則使用者可以關注公眾號,關注後微信會將帶場景值關注事件推送給開發者。
  • 如果使用者已經關注公眾號,則微信會將帶場景值掃描事件推送給開發者。

微信推送事件文件

接受微信的事件推送:

 /**
   * 當設定完微信公眾號的介面後,微信會把使用者傳送的訊息,掃碼事件等推送過來
   *
   * @param signature 微信加密簽名,signature結合了開發者填寫的 token 引數和請求中的 timestamp 引數、nonce引數。
   * @param encType 加密型別(暫未啟用加密訊息)
   * @param msgSignature 加密的訊息
   * @param timestamp 時間戳
   * @param nonce 隨機數
   * @throws IOException
   */
  @PostMapping(produces = "text/xml; charset=UTF-8")
  public void api(HttpServletRequest httpServletRequest,
                  HttpServletResponse httpServletResponse,
                  @RequestParam("signature") String signature,
                  @RequestParam(name = "encrypt_type", required = false) String encType,
                  @RequestParam(name = "msg_signature", required = false) String msgSignature,
                  @RequestParam("timestamp") String timestamp,
                  @RequestParam("nonce") String nonce) throws IOException {
    if (!this.weChatMpService.checkSignature(timestamp, nonce, signature)) {
      this.logger.warn("接收到了未透過校驗的微信訊息,這可能是token配置錯了,或是接收了非微信官方的請求");
      return;
    }
    BufferedReader bufferedReader = httpServletRequest.getReader();
    String str;
    StringBuilder requestBodyBuilder = new StringBuilder();
    while ((str = bufferedReader.readLine()) != null) {
      requestBodyBuilder.append(str);
    }
    String requestBody = requestBodyBuilder.toString();

    this.logger.info("\n接收微信請求:[signature=[{}], encType=[{}], msgSignature=[{}],"
                    + " timestamp=[{}], nonce=[{}], requestBody=[\\n{}\\n]",
            signature, encType, msgSignature, timestamp, nonce, requestBody);
}

5. 後臺進行邏輯處理

image.png

微信推送給公眾號的訊息型別很多,而公眾號也需要針對使用者不同的輸入做出不同的反應。

如果使用if ... else ...來實現的話非常難以維護,這時可以根據weixin-java-mp開發文件,使用 WxMpMessageRouter 來對訊息進行路由維護。

先寫出幾個事件的handler。比如掃碼事件和關注事件。 原始碼在文末
image.png

/**
 * 處理關注事件
 * 新使用者走關注事件;老使用者走掃碼事件
 * @author Binary Wang
 */
@Component
public class SubscribeHandler extends AbstractHandler {
  private final WeChatMpService weChatMpService;
  private final WechatService wechatService;
  private final Logger logger = LoggerFactory.getLogger(this.getClass());

  public SubscribeHandler(WeChatMpService weChatMpService , WechatService wechatService) {
    super(weChatMpService);
    this.weChatMpService = weChatMpService;
    this.wechatService = wechatService;
  }

  @Override
  public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
                                  Map<String, Object> context,
                                  WxMpService wxMpService,
                                  WxSessionManager sessionManager) throws WxErrorException {
    this.logger.info("新關注使用者 OPENID: " + wxMessage.getFromUser());
    if (this.logger.isDebugEnabled()) {
      this.logger.info("新關注使用者 OPENID: " + wxMessage.getFromUser());
    }
    WeChatUser weChatUser = this.wechatService.getOneByOpenidAndAppId(wxMessage.getFromUser(), wxMessage.getToUser());
    if (wxMessage.getEventKey().startsWith("qrscene_")) {
      String key = wxMessage.getEventKey().substring("qrscene_".length());
      return this.handleByEventKey(key, weChatUser, wxMessage);
    }
    return new TextBuilder().build("感謝關注,祝您生活愉快!",
        wxMessage,
        weChatMpService);
  }
}

對寫的幾個handler進行路由註冊

private WxMpMessageRouter router;

 private void refreshRouter() {
    final WxMpMessageRouter newRouter = new WxMpMessageRouter(this);

    // 關注事件
     newRouter.rule().async(false).msgType(EVENT).event(SUBSCRIBE).handler(this.subscribeHandler).end();

    // 取消關注事件
     newRouter.rule().async(false).msgType(EVENT).event(UNSUBSCRIBE).handler(this.unsubscribeHandler).end();
    // 掃碼事件 newRouter.rule().async(false).msgType(EVENT).event(SCAN).handler(this.scanHandler).end();

    // 預設
    newRouter.rule().async(false).handler(this.msgHandler).end();

    this.router = newRouter;
  }

/**
   * 微信事件透過這個入口進來
   * 根據不同事件,呼叫不同handler
   */
 public WxMpXmlOutMessage route(WxMpXmlMessage message) {
    try {
      return this.router.route(message);
    } catch (Exception e) {
      this.logger.error(e.getMessage(), e);
    }

    return null;
  }

現在已經配置好了,微信推送的掃碼事件或者關注事件已經被我們定義的handler獲取到了。

那麼有下一個問題:假如有多個使用者同時在使用微信登入,我們怎麼要推送給哪個客戶端,讓掃碼成功的使用者成功登陸呢?

image.png

這時就需要在返回使用者登陸二維碼前,

  • 記錄該客戶端的seesionId
  • 同時新增一個key為uuid,value為自定義handler到hashmap中。

這裡的uuid為,前面獲取ticket隨機生成的uuid。

這樣就可以在掃碼事件handler中, 呼叫這個自定義的handler,實現向指定客戶端傳送資訊。



在返回二維碼前,定義自定義handler

 @Override
  public String getLoginQrCode(String wsLoginToken, HttpSession httpSession) {
    try {
      if (this.logger.isDebugEnabled()) {
        this.logger.info("1. 生成用於回撥的uuid,請將推送給微信,微信當推送帶有UUID的二維碼,使用者掃碼後微信則會把帶有uuid的資訊回推過來");
      }
      String qrUuid = UUID.randomUUID().toString();
      WxMpQrCodeTicket wxMpQrCodeTicket = this.wxMpService.getQrcodeService().qrCodeCreateTmpTicket(qrUuid, 10 * 60);
      this.wxMpService.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) {
            String uuid = UUID.randomUUID().toString();
            bindWsUuidToWeChatUser(uuid, weChatUser);
            simpMessagingTemplate.convertAndSendToUser(wsLoginToken,
                "/stomp/scanLoginQrCode",
                uuid);
            return new TextBuilder().build(String.format("登入成功,登入的使用者為: %s", weChatUser.getUser().getName()),
                wxMpXmlMessage,
                null);
          } else {
            simpMessagingTemplate.convertAndSendToUser(wsLoginToken,
                "/stomp/scanLoginQrCode",
                false);
            return new TextBuilder().build(String.format("登入原則,原因:您尚未繫結微信使用者"),
                wxMpXmlMessage,
                null);
          }
        }
      });
      return this.wxMpService.getQrcodeService().qrCodePictureUrl(wxMpQrCodeTicket.getTicket());
    } catch (Exception e) {
      this.logger.error("獲取臨時公眾號圖片時發生錯誤:" + e.getMessage());
    }
    return "";
  }

image.png

利用websocket向前端傳送資訊

WebSocket 是 HTML5 開始提供的一種在單個 TCP 連線上進行全雙工通訊的協議。
WebSocket 使得客戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向客戶端推送資料。在 WebSocket API 中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。
 simpMessagingTemplate.convertAndSendToUser(wsLoginToken,
                "/stomp/scanLoginQrCode",
                uuid);

之後前端就可以利用這個身份資訊loginUid登陸了。

前臺向後臺傳送登陸請求

/**
   * 微信掃碼登入
   */
  onWeChatLogin() {
    this.userService.getLoginQrCode()
      .subscribe(src => {
        this.qrCodeSrc = src;
        this.loginModel = 'wechat';
        this.userService.onScanLoginQrCode$.pipe(first()).subscribe(data => {
          const uuid = data.body;
          this.login({username: uuid, password: uuid});
        });
      });
  }

後臺再判斷loginUid是否在map中,是則登陸成功。

/**
   * 校驗微信掃碼登入後的認證ID是否有效
   * @param wsAuthUuid websocket認證ID
   */
  @Override
  public boolean checkWeChatLoginUuidIsValid(String wsAuthUuid) {
    return this.map.containsKey(wsAuthUuid);
  }

原始碼地址: 待整理中,整理後放出

相關文章