1. 程式人生 > 程式設計 >SpringBoot+輪詢or長連線 實現掃碼登入功能Demo—Postman模擬掃碼請求

SpringBoot+輪詢or長連線 實現掃碼登入功能Demo—Postman模擬掃碼請求

掃碼登入功能Demo—Postman模擬掃碼請求

  • 掃碼登入功能—輪詢or長連線WebSocket—Zxing生成二維碼

掃碼登入其實就是一個登入請求,只不過資訊儲存在使用者手機上,還需要通過二維碼驗證是否匹配的方式就可以登入,免去了使用者多次輸入密碼的場景,現在越來越多登入方式,其中掃碼登入算是比較人性化的了

我們把一個全域性唯一id儲存在二維碼中,使用手機掃碼可以獲取到二維碼中的資訊,此時就把該二維碼和你的手機使用者賬號建立一種繫結的關係,這個二維碼就只歸你所有了,當你登入完後這個二維碼就廢棄了,二維碼起的作用就是一種認證的機制

流程

具體流程如下圖:

Step 1、使用者 A 訪問網頁客戶端,伺服器為這個會話生成一個全域性唯一的 ID,此時系統並不知道訪問者是誰。

Step 2、使用者A開啟自己的手機App並掃描這個二維碼,並提示使用者是否確認登入。

Step 3、手機上的是登入狀態,使用者點選確認登入後,手機上的客戶端將賬號和這個掃描得到的 ID 一起提交到伺服器

Step 4、伺服器將這個 ID 和使用者 A 的賬號繫結在一起,並通知網頁版,這個 ID 對應的微訊號為使用者 A,網頁版載入使用者 A 的資訊,至此,掃碼登入全部流程完成

建立二維碼

我們選取使用自己在伺服器端根據建立的全域性唯一id生成一個二維碼,使用googlezxing二維碼生成類庫

  • 依賴
<dependency>
            <groupId
>
com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.2.1</version> </dependency> 複製程式碼
  • 生成二維碼

根據content內容和指定高度和寬度生成二維碼的base64格式圖片,可以直接在前端顯示

public String createQrCode(String content,int width,int height) throws IOException 
{ String resultImage = ""; if (!StringUtils.isEmpty(content)) { ServletOutputStream stream = null; ByteArrayOutputStream os = new ByteArrayOutputStream(); @SuppressWarnings("rawtypes") HashMap<EncodeHintType,Comparable> hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET,"utf-8"); // 指定字元編碼為“utf-8” hints.put(EncodeHintType.ERROR_CORRECTION,ErrorCorrectionLevel.M); // 指定二維碼的糾錯等級為中級 hints.put(EncodeHintType.MARGIN,2); // 設定圖片的邊距 try { QRCodeWriter writer = new QRCodeWriter(); BitMatrix bitMatrix = writer.encode(content,BarcodeFormat.QR_CODE,width,height,hints); BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix); ImageIO.write(bufferedImage,"png",os); /** * 原生轉碼前面沒有 data:image/png;base64 這些欄位,返回給前端是無法被解析,可以讓前端加,也可以在下面加上 */ resultImage = new String("data:image/png;base64," + Base64.encode(os.toByteArray())); return resultImage; } catch (Exception e) { e.printStackTrace(); } finally { if (stream != null) { stream.flush(); stream.close(); } } } return null; } 複製程式碼

二維碼狀態管理

我們使用redis來儲存每一張二維碼的狀態

狀態:

  1. NOT_SCAN 未被掃描
  2. SCANNED 被掃描
  3. VERIFIED 確認完後
  4. EXPIRED 過期
  5. FINISH 完成

由於一張二維碼只能被掃描一次,所以我們每一次掃描一張二維碼後,把狀態設定為SCANNEDSCANNED狀態的二維碼無法再次被掃描,丟擲已被掃描的資訊

狀態轉移:

NOT_SCANNED->SCANNED->VERIFIED->FINISH

其中EXPIRED狀態可以插在其中任意一個位置,過期了的二維碼也自動過期

生成二維碼介面

  • 建立二維碼

使用UUID工具類生成全域性唯一id,也可以使用snowflake生成自增的全域性唯一id,然後儲存到redis中,key為uuid,val為當前二維碼狀態,我們這裡維護了一個map儲存所有uuid對應的二維碼base格式,用於建立對應關係,前端傳遞二維碼base64過來我們來判斷這張二維碼對應的uuid是多少

很多人問為什麼不讓前端傳遞掃描過後的uuid呢?第一,我們只能使用postman模擬請求,我們無法根據手機app掃碼獲取二維碼資訊,所以暫時採取傳輸圖片,實際中肯定採用uuid去傳輸,因為base64本來就很大,儘量傳輸資料量小的資料

@GetMapping("/createQr")
    @ResponseBody
    public Result<String> createQrCode() throws IOException {
        String uuid = UUIDUtil.uuid();
        log.info(uuid);
        String qrCode = qrCodeService.createQrCode(uuid,200,200);
        qrCodeMap.put(qrCode,uuid);
        redisService.set(QrCodeKey.UUID,uuid,QrCodeStatus.NOT_SCAN);
        return Result.success(qrCode);
    }
複製程式碼

前端輪詢法判斷二維碼是否被掃描

目前阿里雲登入控制檯就是使用輪詢的方法,具體為什麼不使用長連線我也不清楚,但是說明這種方法也是比較常見的

後端只需要處理app登入請求和確認請求以及網頁端響應的請求就好了

二維碼是否被掃描介面—前端只需要輪詢該介面

獲取到redis儲存對應uuid的狀態,返回給前端,前端輪詢判斷做處理

@GetMapping("/query")
    @ResponseBody
    public Result<String> queryIsScannedOrVerified(@RequestParam("img")String img){
        String uuid = qrCodeMap.get(img);
        QrCodeStatus s = redisService.get(QrCodeKey.UUID,QrCodeStatus.class);
        return Result.success(s.getStatus());
    }
複製程式碼

app掃描介面

app掃描二維碼後,拿到對應的二維碼資訊傳送一個掃描請求給後端,攜帶app使用者引數,這裡demo演示就模擬一個絕對的使用者資訊

之後就是判斷redis中uuid的狀態度

  • 如果為NOT_SCAN,就修改為SCANNED
  • 如果為SCANNED,就返回重複掃描的錯誤
  • 如果為VERIFIED,就完成本次二維碼登入邏輯,使用者登入成功
@GetMapping("/doScan")
    @ResponseBody
    public Result doAppScanQrCode(@RequestParam("username")String username,@RequestParam("password")String password,@RequestParam("uuid")String uuid){
        QrCodeStatus status = redisService.get(QrCodeKey.UUID,QrCodeStatus.class);
        log.info(status.getStatus());
        if(status.getStatus().isEmpty()) return Result.error(ErrorCodeEnum.UUID_EXPIRED);
        switch (status){
            case NOT_SCAN:
                //等待確認 todo
                if(username.equals("dzou")&&password.equals("1234")){
                    redisService.set(QrCodeKey.UUID,QrCodeStatus.SCANNED);
                    return Result.success("請手機確認");
                }else{
                    return Result.error(ErrorCodeEnum.LOGIN_FAIL);
                }
            case SCANNED:
                return Result.error(ErrorCodeEnum.QRCODE_SCANNED);
            case VERIFIED:
                return Result.success("你已經確認過了");
        }
        return Result.error(ErrorCodeEnum.SEVER_ERROR);
    }
複製程式碼

app確認登入介面

app掃描成功後,二維碼狀態變為SCANNED,需要傳送一個請求給app前端請求使用者確認,使用者點選確認後請求這個介面,完成登入

@GetMapping("/verify")
    @ResponseBody
    public Result verifyQrCode(@RequestParam("uuid")String uuid){
        String status = redisService.get(QrCodeKey.UUID,String.class);
        if(status.isEmpty()) return Result.error(ErrorCodeEnum.UUID_EXPIRED);
        redisService.set(QrCodeKey.UUID,QrCodeStatus.VERIFIED);
        return Result.success("確認成功");
    }
複製程式碼

前端—JQuery

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>掃描二維碼</title>
  <!-- jquery -->
  <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
  <!-- bootstrap -->
  <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}"/>
  <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
</head>
<body>
  <h1>二維碼</h1>
  <div>
    <table>
      <tr>
        <td><img id="qrCode" width="200" height="200"/></td>
      </tr>
    </table>
  </div>
</body>
<script>
  var img = "";
  $.ajax({
    url: "/api/createQr",type:"GET",success:function (data) {
      $("#qrCode").attr("src",data.data);
      img = data.data;
      callbackScan($("#qrCode").attr("src"))
    }
  });
    //使用setTimeOut來迴圈請求判斷是否被掃描,被掃描以後呼叫下面一個函式迴圈判斷是否被確認
  function callbackScan(img) {
    var tID = setTimeout(function() {
      $.ajax({
        url : '/api/query',dataType: "json",type: 'GET',data:{"img":img},success : function(res) {
          //process data here
          console.log("img:"+img);
          console.log(res.data);
          if(res.data=="scanned") {
            clearTimeout(tID);
            console.log("請求確認")
            callbackVerify(img)
          }else {
            callbackScan(img)
          }
        }
      }) },1500);
  }
//迴圈判斷是否被確認
  function callbackVerify(img) {
    var tID = setTimeout(function() {
      $.ajax({
        url : '/api/query',success : function(res) {
          //process data here
          console.log(res.data);
          if(res.data=="verified") {
            clearTimeout(tID);
            console.log("確認成功")
            window.location.href = "success";
          }else {
            callbackVerify(img)
          }
        }
      }) },1500);
  }

</script>
</html>
複製程式碼

成功後跳轉到成功頁面

測試

  • 開啟主頁建立二維碼

  • 拿到伺服器端建立的uuid請求掃描介面

  • 拿uuid請求確認介面

  • 確認完成,跳轉到登入介面

長連線WebSocket來傳輸二維碼被掃描的資訊

除了輪詢還有一種相對來說更好的實現方式就是WebSocket長連線,但是有些瀏覽器不支援WebSocket,考慮到這點我們決定使用SockJs,他是一種優先Websocket的連線方式,不支援的話它會去使用其他類似輪詢的方式

我們伺服器端需要編寫對應的WebSocket處理邏輯,我們在載入頁面時建立長連線,掃描時請求介面,把狀態傳送給前端WebSocket,如果為被掃描,傳送請求確認的資訊,請求確認介面完成確認後傳送狀態給前端WebSocket,跳轉到success頁面

我們使用Springboot提供的WebSocket支援類庫編寫,如果有需要使用netty編寫的同學,可以參考我的另外一篇netty的文章

maven依賴

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>2.0.4.RELEASE</version>
        </dependency>
複製程式碼

WebSocket配置類

  • 其中第一個方法registerStompEndpoints相當於指定代理伺服器的WebSocket路由
  • 第二個方法就是客戶端訂閱路由,客戶端可以接收到這個路由傳送的資訊
@Configuration
@EnableWebSocketMessageBroker
public class IWebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
//註冊一個Stomp 協議的endpoint,並指定 SockJS協議
        registry.addEndpoint("/websocket").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        //registry.setApplicationDestinationPrefixes("/app");
    }
}
複製程式碼

注入WebSocket傳送訊息模板

@Autowired
    private SimpMessagingTemplate simpMessagingTemplate;
複製程式碼

掃描二維碼介面

我們只需要稍微改一下程式碼,在第一次掃描後使用WebSocket傳送一個資訊請求確認給前端WebSocket

@GetMapping("/doScan")
    @ResponseBody
    public Result doAppScanQrCode(@RequestParam("username")String username,QrCodeStatus.class);
        log.info(
                status.getStatus());
        if(status.getStatus().isEmpty()) return Result.error(ErrorCodeEnum.UUID_EXPIRED);
        switch (status){
            case NOT_SCAN:
                if(username.equals("dzou")&&password.equals("1234")){
                    redisService.set(QrCodeKey.UUID,QrCodeStatus.SCANNED);
                    simpMessagingTemplate.convertAndSend("/topic/ws","請確認");
                    return Result.success("請手機確認");
                }else{
                    return Result.error(ErrorCodeEnum.LOGIN_FAIL);
                }
            case SCANNED:
                return Result.error(ErrorCodeEnum.QRCODE_SCANNED);
            case VERIFIED:
                return Result.success("你已經確認過了");
        }
        return Result.error(ErrorCodeEnum.SEVER_ERROR);
    }
複製程式碼

確認登入介面

我們需要稍改確認的程式碼,因為確認成功我們需要向客戶端訂閱的指定路由傳送一條訊息

呼叫convertAndSend傳送指定訊息到指定路由下

@GetMapping("/verify")
    @ResponseBody
    public Result verifyQrCode(@RequestParam("uuid")String uuid){
        String status = redisService.get(QrCodeKey.UUID,QrCodeStatus.VERIFIED);
        simpMessagingTemplate.convertAndSend("/topic/ws","已經確認");
        return Result.success("確認成功");
    }
複製程式碼

前端

前端就不需要輪詢的那兩個方法了,只需要連線SockJs就好了,根據WebSocket傳送的資訊進行處理,我們這裡需要客戶端連線上後進行訂閱,指定接收伺服器哪個路由傳送的訊息

function connect() {
    var socket = new SockJS('/websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({},function (frame) {
      console.log('Connected: ' + frame);
      stompClient.subscribe('/topic/ws',function (response) {//訂閱路由訊息
        console.log(response);
        if(response.body=="請確認"){
          layer.msg("請在你的app上確認登入")
        }else if(response.body=="已經確認"){
          window.location.href = "success"
        }
      });
    });
  }
複製程式碼

測試

  • 開啟主頁建立二維碼,連線WebSocket

  • 拿到伺服器端建立的uuid請求掃描介面

  • 控制檯列印請求確認資訊

  • 拿uuid請求確認介面

  • 確認完成,跳轉到登入介面,傳送已經確認

demo地址:github.com/ding-zou/qr…