SpringBoot微信掃碼登入(小程式版)

Pursuer丶發表於2022-11-22

一、需求描述

使用者在PC端用微信掃描二維碼實現後臺登入
圖示:
image.png
e97764478c9bca0905cc82d04af690e.jpg
image.png

二、實現原理

微信掃碼登入.jpg
此處採用socket實現,當然也可以透過輪詢去監測微信掃碼狀態

三、實現步驟

1. 微信公眾平臺小程式後臺申請跳轉連結

開發管理->開發設定->掃普通連結二維碼開啟小程式

image.png

2. PC請求websocket連線獲取二維碼key

客戶端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--登入介面-->
<img id="qrCode" src="" alt="掃碼">
</body>
</html>
<script>
    let socket;
    let timer;
    if (typeof WebSocket === "undefined") {
        alert("您的瀏覽器不支援socket");
    } else {
        // 例項化socket
        socket = new WebSocket(
            "wss://ip:port/platform-system/socket/wx/qrLogin");
        // 監聽socket連線
        socket.onopen = () => {
            console.log('連線成功.......')
            //模擬心跳
            timer = setInterval(() => {
                socket.send(1)
            }, 30000)
        };
        // 監聽socket錯誤資訊
        socket.onerror = () => {
            console.log('連線發生錯誤.......')
        };
        // 監聽socket訊息
        socket.onmessage = ({data}) => {
            //判斷狀態
            //此處0代表剛建立連線,返回來的是二維碼key
            if (data.code === 0) {
                //請求服務端獲取二維碼
                document.querySelector('#qrCode').src = `https://ip:port/platform-system/auth/wx/miniprogram/qrCode?pcKey=${data.data}`
            } else if (data.code === 1) {
                //關閉定時器
                clearInterval(timer)
                //關閉socket
                socket.close()
                //此處1代表登入成功
                location.href = '後臺主頁'
            } else {
                //其他情況代表小程式登入出現問題
                console.log(`登入出錯:${data.msg}`)
                //關閉定時器
                clearInterval(timer)
                //關閉socket
                socket.close()
            }
        };
    }
</script>

服務端:

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
public class JsonWebSocketEncoder implements Encoder.Text<Result> {

    @Override
    public String encode(Result object) throws EncodeException {
        try {
            return JSONUtil.toJsonStr(object);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void init(EndpointConfig endpointConfig) {

    }

    @Override
    public void destroy() {

    }
}
@Slf4j
@Component
@ServerEndpoint(value = "/socket/wx/qrLogin", encoders = JsonWebSocketEncoder.class)
public class WxQrLoginWebSocket {

    public static final ConcurrentMap<String, Session> sockets = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session) {
        log.info("客戶端連線成功:{}", session.getId());
        //將連線結果告訴客戶端用於攜帶進入小程式(此處直接用的雪花演算法,可使用其他方式生成key)
        String key = IdUtil.getSnowflake().nextIdStr();
        //存放session
        sockets.put(key, session);
        log.info("當前線上客戶端:{}個", sockets.size());
        //通知客戶端
        sendMessage(key, Result.result("0", null, key));
    }

    /**
     * 連結關閉呼叫的方法
     */
    @OnClose
    public void onClose(Session session) {
        try {
            sockets.entrySet().removeIf(entry -> session.getId().equals(entry.getValue().getId()));
            log.info("【websocket訊息】連線斷開,sessionId:" + session.getId());
            log.info("當前線上客戶端:{}個", sockets.size());
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage());
        }
    }

    /**
     * 收到客戶端訊息後呼叫的方法
     *
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("【websocket訊息】收到客戶端心跳:{},{}", message, session.getId());
        log.info("當前線上客戶端:{}個", sockets.size());
    }

    /**
     * 傳送錯誤時的處理
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        try {
            //列印錯誤
            log.error("使用者錯誤,原因:" + error.getMessage());
            error.printStackTrace();
            //關閉連結
            session.close();
            sockets.entrySet().removeIf(entry -> session.getId().equals(entry.getValue().getId()));
            log.info("當前線上客戶端:{}個", sockets.size());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 訊息
     *
     * @param key  key
     * @param data 資料
     **/
    public static void sendMessage(String key, Object data) {
        sockets.get(key).getAsyncRemote().sendObject(data);
    }
}

3. 小程式獲取key並授權登入

客戶端:

  1. 獲取key

     onLoad(option) {
      if(option.q){
          //微信掃描掃描二維碼進來的
          let url = decodeURIComponent(option.q)
          let params = this.getUrlParam(url)
          console.log('從微信掃碼過來params')
          //此處需要儲存這個pcKey後面會用到
          console.log(params.pcKey)
      }
     }
     getUrlParam(url){
        let params = url.split("?")[1].split("&");
        let obj = {};
        params.map(v => (obj[v.split("=")[0]] = v.split("=")[1]));
        return obj
     }
  2. 請求服務端登入並通知PC端

    wx.request({
     url: 'https://ip:port/...', //僅為示例,並非真實的介面地址
     method: 'POST',
     header: {
         'content-type': 'application/x-www-form-urlencoded'
     },
     data: {
         username: 'username',
         password: 'password'
     },
     success: function (res) {
         //獲取token
         var token = res.data.accessToken;
         //通知服務端告訴PC登入
         wx.request({
             url: 'https://ip:port/auth/notifyPcLogin', //僅為示例,並非真實的介面地址
             method: 'GET',
             header: {
                 'Authorization': token
             },
             data: {
                 pcKey: '之前儲存的key'
             },
             success: function (res) {
                 console.log(res.data)
             }
         })
     }
    })

    服務端:

    @RestController
    @RequestMapping("/auth")
    @RequiredArgsConstructor
    public class LoginController {
    
     private final WxLoginService wxLoginService;
    
     @ApiOperation(value = "通知PC登入介面")
     @GetMapping("/notifyPcLogin")
     public Result<Boolean> notifyPcLogin(
             @ApiParam(name = "pcKey", value = "PC端後端給的唯一標識", required = true)
             @NotBlank(message = "唯一標識不能為空") @RequestParam(value = "pcKey") String pcKey
     ) {
         return Result.success(wxLoginService.notifyPcLogin(pcKey));
     }
    }
    @Service
    @RequiredArgsConstructor
    public class WxLoginService {
    
     private final ISysUserService sysUserService;
    
     /**
      * 喚醒PC登入
      *
      * @param pcKey pc唯一key
      * @return java.lang.Boolean
      * @author Guo Shuai
      * @since 1.0
      **/
     public Boolean notifyPcLogin(String pcKey) {
         //獲取socket session
         Session session = WxQrLoginWebSocket.sockets.get(pcKey);
         //判斷是否存在
         if (ObjectUtil.isNull(session)) {
             return Boolean.FALSE;
         }
         //獲取當前登入使用者手機號
         String phone = sessionService.getLoginUser().getPhone();
         //透過手機號查詢使用者
         SysUser sysUser = sysUserService.lambdaQuery().eq(SysUser::getMobile, phone).one();
         //獲取使用者pc端token
         LoginUserVO loginUserVO = sysUserService.getUserToken(sysUser, sysUser.getMobile(), sessionService.getTenantId(), "10");
         //通知pc登入
         WxQrLoginWebSocket.sendMessage(pcKey, Result.result("1", null, loginUserVO));
         //返回
         return Boolean.FALSE;
     }
    }

    至此整個流程結束

    四、注意事項

    小程式申請的跳轉連結只有正式版可以攜帶自定義的動態引數,否則只能用配置的測試地址,測試地址可以配置最多5個

相關文章