Code scanning login function Demo – Postman Simulates code scanning requests

  • Scan code login function – Polling or long connection WebSocket – Zxing Generates TWO-DIMENSIONAL code

Scanning code login is actually a login request, but the information is stored in the user’s mobile phone, but also through the two-dimensional code to verify whether the match can be logged in, the user does not need to enter the password for many times, now there are more and more login methods, among which the scanning code login is more humane

We keep a globally unique id in the qr code, the use of mobile phones and code access to the information in the qr code, where you put the qr code with your mobile phone users to establish a binding relationship accounts, the qr code can only belong to you, when you log on after the qr code is abandoned, function of qr code is a kind of authentication mechanism

process

The specific process is as follows:

Step 1. When user A visits the web client, the server generates A globally unique ID for this session. At this time, the system does not know who the visitor is.

Step 2. User A opens his mobile phone App, scans the TWO-DIMENSIONAL code, and prompts the user whether to confirm login.

Step 3: The login status is displayed on the mobile phone. After the user clicks “confirm login”, the client on the mobile phone submits the account number and the scanned ID to the server

Step 4. The server binds this ID to the account of user A, and notifies the webpage version that the wechat corresponding to this ID is user A. The webpage version loads the information of user A

Create a QR code

We choose to use our own on the server side according to the global unique ID created to generate a TWO-DIMENSIONAL code, using Google’s Zxing TWO-DIMENSIONAL code to generate the class library

  • Rely on
<dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>javase</artifactId>
            <version>3.2.1</version>
        </dependency>
Copy the code
  • Generate qr code

According to the content content and the specified height and width of the base64 format generated TWO-DIMENSIONAL code picture, can be directly displayed in the front

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"); // Set the character encoding to utF-8.
            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); // Specify the error correction level of the TWO-DIMENSIONAL code as intermediate
            hints.put(EncodeHintType.MARGIN, 2); // Set the image margins
            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 these fields, returned to the front end is not parsed, you can ask the front end to add, or you can add */ below
                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;
    }
Copy the code

Status management of TWO-DIMENSIONAL code

We use Redis to store the status of each QR code

Status:

  1. NOT_SCAN is not scanned
  2. SCANNED by scanning
  3. VERIFIED After the verification
  4. EXPIRED date
  5. FINISH to complete

As a TWO-DIMENSIONAL code can only be SCANNED once, we SCANNED a two-dimensional code each time and set the state as SCANNED. The SCANNED two-dimensional code in SCANNED state cannot be SCANNED again and the SCANNED information is thrown out

State transition:

NOT_SCANNED->SCANNED->VERIFIED->FINISH

The EXPIRED status can be inserted in any of the positions, and the EXPIRED QR code will automatically expire

Generate two-dimensional code interface

  • Create a QR code

Use the UUID tool class to generate global unique IDS, or use Snowflake to generate self-increasing global unique IDS, and save them to Redis. Key is the UUID, val is the status of the current TWO-DIMENSIONAL code, we maintain a map to save the base format of two-dimensional code corresponding to all UUID, used to establish the corresponding relationship. The front end passes the TWO-DIMENSIONAL code base64 to us to determine how much the UUID of this two-dimensional code is

A lot of people ask why not have the front end pass the scanned UUID? First, we can only use Postman to simulate the request, and we cannot obtain two-dimensional code information by scanning code of mobile phone app, so we temporarily take the transmission of pictures. In practice, WE must use UUID to transmit, because Base64 is originally very large, and try to transmit data with a small amount of data

@GetMapping("/createQr")
    @ResponseBody
    public Result<String> createQrCode(a) 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);
    }
Copy the code

Front-end polling method to determine whether the TWO-DIMENSIONAL code is scanned

At present, Ali cloud login console is to use polling method, specific why not use long connection I do not know, but explain that this method is more common

The back end only needs to handle app login and confirmation requests and requests that the web side responds to

Whether the QR code is scanned – The front-end only polls the interface

After obtaining the status of the corresponding UUID saved by Redis, it is returned to the front end, and the front end polls for judgment and processing

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

App scanning interface

After the APP scans the TWO-DIMENSIONAL code, it gets the corresponding two-dimensional code information and sends a scanning request to the back end, carrying app user parameters. The demo here simulates an absolute user information

Then determine the status of uUID in Redis

  • If it isNOT_SCANIs changed toSCANNED
  • If it isSCANNED, returns a repeat scan error
  • If it isVERIFIED, complete the TWO-DIMENSIONAL code login logic, the user login success
@GetMapping("/doScan")
    @ResponseBody
    public Result doAppScanQrCode(@RequestParam("username")String username,
                               @RequestParam("password")String password,
                               @RequestParam("uuid")String uuid){
        QrCodeStatus status = redisService.get(QrCodeKey.UUID,uuid,QrCodeStatus.class);
        log.info(status.getStatus());
        if(status.getStatus().isEmpty()) return Result.error(ErrorCodeEnum.UUID_EXPIRED);
        switch (status){
            case NOT_SCAN:
                // Wait for confirmation todo
                if(username.equals("dzou")&&password.equals("1234")){
                    redisService.set(QrCodeKey.UUID,uuid, QrCodeStatus.SCANNED);
                    return Result.success("Please confirm by phone");
                }else{
                    return Result.error(ErrorCodeEnum.LOGIN_FAIL);
                }
            case SCANNED:
                return Result.error(ErrorCodeEnum.QRCODE_SCANNED);
            case VERIFIED:
                return Result.success("You've already confirmed that.");
        }
        return Result.error(ErrorCodeEnum.SEVER_ERROR);
    }
Copy the code

App confirms login interface

After the APP scans successfully, the status of the QR code changes toSCANNED, a request needs to be sent to the front end of the APP to request the user to confirm, and the user clicks “confirm” to request the interface to complete the login

@GetMapping("/verify")
    @ResponseBody
    public Result verifyQrCode(@RequestParam("uuid")String uuid){
        String status = redisService.get(QrCodeKey.UUID,uuid,String.class);
        if(status.isEmpty()) return Result.error(ErrorCodeEnum.UUID_EXPIRED);
        redisService.set(QrCodeKey.UUID,uuid,QrCodeStatus.VERIFIED);
        return Result.success("Confirmed success");
    }
Copy the code

The front – JQuery


      
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Scan the QR code</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>Qr code</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"))}});// Use setTimeOut to loop the request to determine whether it has been scanned. After being scanned, call the following function to loop to determine whether it has been acknowledged
  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("Request confirmation")
            callbackVerify(img)
          }else {
            callbackScan(img)
          }
        }
      }) }, 1500);
  }
// Loop to see if it is confirmed
  function callbackVerify(img) {
    var tID = setTimeout(function() {
      $.ajax({
        url : '/api/query'.dataType: "json".type: 'GET'.data: {"img":img},
        success : function(res) {
          //process data here
          console.log(res.data);
          if(res.data=="verified") {
            clearTimeout(tID);
            console.log("Confirmed success")
            window.location.href = "success";
          }else {
            callbackVerify(img)
          }
        }
      }) }, 1500);
  }

</script>
</html>
Copy the code

If yes, go to the success page

test

  • Open the home page to create a QR code

  • Get the server created UUID request scanning interface

  • Request interface confirmation with UUID

  • The login page is displayed

Long connect WebSocket to transmit qr code scanned information

In addition to polling, there is a relatively better way to implement WebSocket long connection, but some browsers do not support WebSocket, considering this, we decided to use SockJs, which is a preferred WebSocket connection method, if not, it will use other similar polling method

We need to write the corresponding WebSocket processing logic on the server side. We establish a long connection when loading the page, request the interface during scanning, and send the status to the front-end WebSocket. If the page is scanned, send the request confirmation information, and send the status to the front-end WebSocket after the request confirmation interface completes the confirmation. Go to the SUCCESS page

We use WebSocket support class library provided by Springboot to write, if you need to use netty to write students, you can refer to my other Netty article

Maven rely on

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>2.0.4. RELEASE</version>
        </dependency>
Copy the code

WebSocket configuration class

  • The first oneregisterStompEndpointsEquivalent to specifying the WebSocket route for the proxy server
  • The second method is for the client to subscribe to the route. The client can receive the message sent by the route
@Configuration
@EnableWebSocketMessageBroker
public class IWebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
// Register a Stomp endpoint and specify the SockJS protocol
        registry.addEndpoint("/websocket").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        //registry.setApplicationDestinationPrefixes("/app");}}Copy the code

Inject the WebSocket send message template

@Autowired
    private SimpMessagingTemplate simpMessagingTemplate;
Copy the code

Scanning TWO-DIMENSIONAL code interface

We just need to change the code slightly and use WebSocket to send a message request acknowledgement to the front-end WebSocket after the first scan

@GetMapping("/doScan")
    @ResponseBody
    public Result doAppScanQrCode(@RequestParam("username")String username,
                                  @RequestParam("password")String password,
                                  @RequestParam("uuid")String uuid){
        QrCodeStatus status = redisService.get(QrCodeKey.UUID,uuid,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,uuid, QrCodeStatus.SCANNED);
                    simpMessagingTemplate.convertAndSend("/topic/ws"."Please confirm");
                    return Result.success("Please confirm by phone");
                }else{
                    return Result.error(ErrorCodeEnum.LOGIN_FAIL);
                }
            case SCANNED:
                return Result.error(ErrorCodeEnum.QRCODE_SCANNED);
            case VERIFIED:
                return Result.success("You've already confirmed that.");
        }
        return Result.error(ErrorCodeEnum.SEVER_ERROR);
    }
Copy the code

Confirm login interface

We need to change the validation code slightly, because we need to send a message to the specified route that the client subscribed to

Call convertAndSend to send the specified message to the specified route

@GetMapping("/verify")
    @ResponseBody
    public Result verifyQrCode(@RequestParam("uuid")String uuid){
        String status = redisService.get(QrCodeKey.UUID,uuid,String.class);
        if(status.isEmpty()) return Result.error(ErrorCodeEnum.UUID_EXPIRED);
        redisService.set(QrCodeKey.UUID,uuid,QrCodeStatus.VERIFIED);
        simpMessagingTemplate.convertAndSend("/topic/ws"."Confirmed");
        return Result.success("Confirmed success");
    }
Copy the code

The front end

The front end does not need the two methods of polling, just need to connect SockJs, according to the information sent by WebSocket for processing, we need to connect the client to subscribe, specify which route to receive the message sent by the server

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) {// Subscribe to routing messages
        console.log(response);
        if(response.body=="Please confirm"){
          layer.msg("Please confirm login on your app.")}else if(response.body=="Confirmed") {window.location.href = "success"}}); }); }Copy the code

test

  • Open the home page to create a TWO-DIMENSIONAL code, connect WebSocket

  • Get the server created UUID request scanning interface

  • The console prints request confirmations

  • Request interface confirmation with UUID

  • After confirmation, jump to the login page and send confirmation

Demo address: github.com/ding-zou/qr…