Scavenging Logon Function Demo-Postman Simulated Scavenging Request
- Scavenging Logon Function - Polling or Long Connection WebSocket - Zxing Generate 2D Code
Scavenging login is actually a login request, but the information is stored on the user's mobile phone. It also needs two-dimensional code to verify whether the matching method can be used to login, which eliminates the scenario where users enter their passwords several times. Now more and more login methods are used, among which Scavenging login is more human-friendly.
We save a globally unique id in the QR code, and use the mobile scanner to get the information in the QR code. At this time, we establish a binding relationship between the QR code and your mobile user account. This QR code is owned by you only. When you log in, the QR code is discarded. The role of QR code is a mechanism of authentication.
Technological process
The specific process is as follows:
Step 1, User A accesses the Web page client, and the server generates a globally unique ID for this session, at which point the system does not know who the visitor is.
Step 2, User A opens their mobile App and scans the QR code, prompting the user to confirm their login.
Step 3. The login status is on the mobile phone. After the user clicks to confirm the login, the client on the mobile phone submits the account to the server along with the ID scanned.
Step 4. The server binds this ID with user A's account and notifies the web page version that the micro-signal corresponding to this ID is user A. The web page version loads user A's information, so the whole process of scanner login is completed
Create QR Code
We chose to use our own two-dimensional code on the server side based on the globally unique id created, and use 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>
- Generate two-dimensional code
A base64 format picture that generates a two-dimensional code based on the content and specified height and width, which can be displayed directly in the front end
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"); // Specify character encoding as "utf-8" hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); // Specify an intermediate error correction level for 2-D codes hints.put(EncodeHintType.MARGIN, 2); // Set the margin of the picture 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); /** * There is no data:image/png before the native transcoding; the fields base64, returned to the front end, cannot be parsed, either by adding the front end or 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; }
QR Code Status Management
We use redis to store the state of each two-dimensional code
Status:
- NOT_SCAN not scanned
- SCANNED Scanned
- VERIFIED Confirmation Completed
- EXPIRED expires
- FINISH Completion
Since a QR code can only be scanned once, we set the status to SCANNED after each QR code scan. The QR code in SCANNED state cannot be scanned again, throwing out the scanned information.
State Transition:
NOT_SCANNED->SCANNED->VERIFIED->FINISH
The EXPIRED status can be inserted in any of these locations, and expired QR codes automatically expire
Generate QR Code Interface
- Create QR Code
Use the UUID tool class to generate a global unique id, or snowflake to generate a self-increasing global unique id, then save to redis, key is uuid, val ue is the current two-dimensional code state, we maintain a map to save all UUIDs corresponding to the two-dimensional code base format, used to establish the corresponding relationship, the front end passes the two-dimensional code base64 to determine the corresponding UUID of this two-dimensional codeHow much
Many people ask why not let the front end pass the scanned uuid?First, we can only use postman to simulate the request, we can not get the QR code information from the mobile app scanner, so we temporarily take the transfer of pictures, in fact, it must use UUID to transfer, because base64 is already large, try to transfer data with small amount of data
@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); }
Front-end polling to determine whether a two-dimensional code is scanned
At present, Aliyun login console is using polling method. I don't know why long connection is used, but it is common to explain this method.
The backend only needs to handle app login and confirmation requests and web-side responses
Whether the QR code is scanned or not - the front end only needs to poll the interface
Get redis to save the state of the corresponding uuid, return to the front-end, the front-end polling judgment to process
@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()); }
app scan interface
After app scans the QR code, get the corresponding QR code information and send a scan request to the back-end, carrying app user parameters, demo demo here simulates an absolute user information
* Then it determines the state of uuid in redis.
- If NOT_SCAN, change to SCANNED
- If SCANNED, the error of repeated scan is returned
- If VERIFIED, this QR code login logic is completed and the user login is successful
@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: //Waiting for confirmation todo if(username.equals("dzou")&&password.equals("1234")){ redisService.set(QrCodeKey.UUID,uuid, QrCodeStatus.SCANNED); return Result.success("Please confirm by mobile 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); }
app Confirmation Logon Interface
After the app scan is successful, the QR code status changes to SCANNED, and a request needs to be sent to the app front-end to request user confirmation. After the user clicks on the confirmation, the user requests this 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("Confirm success"); }
Front-end - JQuery
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Scan 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 requests to determine if they have been scanned and call the next function loop after they have been scanned to determine if they have been confirmed 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); } //Cycle to determine if confirmation 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("Confirm success") window.location.href = "success"; }else { callbackVerify(img) } } }) }, 1500); } </script> </html>
Jump to success page after success
test
- Open Home Page to Create QR Code
- Get the uuid request scan interface created on the server side
- Take uuid request to confirm interface
- Confirm completion, jump to login interface
Long connection to WebSocket to transmit scanned QR code information
In addition to polling, there is a relatively better way to achieve this than long WebSockets connections, but some browsers do not support WebSockets. Considering this, we decided to use SockJs, which is a preferred way to connect to WebSockets. If not, it will use other polling-like ways.
Our server side needs to write corresponding WebSocket processing logic. We make long connections when loading pages, request interfaces when scanning, send status to front-end WebSockets, if scanned, send request confirmation information, request confirmation of interface, send status to front-end WebSockets after confirmation, jump to the success page
We write using the WebSocket support class library provided by Springboot, and if you have classmates who need to write using netty, you can refer to another netty article from me
maven dependency
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> <version>2.0.4.RELEASE</version> </dependency>
WebSocket Configuration Class
- The first method, registerStompEndpoints, corresponds to the WebSocket routing of the specified proxy server
- The second way is for the client to subscribe to the route, and the client can receive the information sent by the route.
@Configuration @EnableWebSocketMessageBroker public class IWebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { //Register an endpoint for the Stomp protocol and specify the SockJS protocol registry.addEndpoint("/websocket").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic"); //registry.setApplicationDestinationPrefixes("/app"); } }
Inject WebSocket Send Message Template
@Autowired private SimpMessagingTemplate simpMessagingTemplate;
Scan QR Code Interface
We just need to change the code a little and use WebSocket after the first scan to send an informational request to confirm to the front-end WebSocket
@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 mobile 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); }
Confirm login interface
We need to change the confirmation code a little, because confirmation is successful We need to send a message to the specified route that the client subscribes 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("Confirm success"); }
Front end
The front end doesn't need the two methods of polling anymore, just connect to SockJs, and process the messages according to the information sent by the WebSocket. Here we need the client to connect and subscribe, specifying which route the receiving server sends the messages
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("In your app Confirm login on") }else if(response.body=="Confirmed"){ window.location.href = "success" } }); }); }
test
- Open the home page to create a QR code and connect to a WebSocket
- Get the uuid request scan interface created on the server side
- Console Print Request Confirmation Information
- Take uuid request to confirm interface
- Confirm Complete, Jump to Login Interface, Send Confirmed