Sample project for bare-handed code sweeper login

Sample project for bare-handed code sweeper login

I don't know if it's WeChat or not, now there are more and more scenes of scanner login. As a pursuit and ideal new four good yard farmer, of course, we must keep up with the trend of the times and make an example with no hands.

This sample project is intended for the following technical stacks

  • qrcode-plugin: Open source QR code generation toolkit, project link: https://github.com/liuyueyi/quick-media
  • SpringBoot: Project Base Environment
  • thymeleaf: page rendering engine
  • SSE/Asynchronous Request: Service-side Push Events
  • js: Basic operations for native js

<!-- more -->

I. Principle Analysis

Previously, file downloads were preferred, but when I read a blog post about the principles of scrambling login, I found that it could be combined with previous asynchronous requests/SSE s to create an application battle, so I had this blog post

For information on how scanner login works, see: Talk about QR Code Scanning Login Principle

1. Scene description

To take care of students who may not know much about Scavenger Login, here's a brief description of what it is.

Generally speaking, scanner login involves two ends, three steps

  • pc side, log in to a website, this website login way and traditional user name/password (mobile phone number/authentication code) is different, display is a two-dimensional code
  • App side, with the app of this website, first make sure you are logged in, then scan the QR code, pop up a page of login authorization, click Authorization
  • pc successfully logged in, automatically jumped to the home page

2. Brief description of principles and processes

In the design of the whole system, the core point is that after mobile scanner, pc login succeeds. What is the principle?

  • We assume that app and backend are identified by token
  • The app scanner authorizes and passes token to the back-end, which determines who initiates the login request on the pc side based on token
  • The backend writes the successful login status back to the pc requester and jumps to the home page (this is equivalent to the process after a successful login for the average user, you can choose session, cookie or jwt)

Step-by-step point analysis using the above principles

  • pc login, generate QR code
    • A QR code requires uniqueness and binds to the requester's identity (otherwise, assuming two people have the same QR code, one person scans to log in, and the other isn't?)
    • The client maintains a connection with the server to receive subsequent login successes and adjust the events on the first page (there are many options, such as polling, long connection push)
  • app scanner, authorized login
    • After scanning, jump to the authorization page (so the QR code should correspond to a url)
    • Authorization (Identity determination, binding identity information to pc requester and jumping to home page)

Finally, the business process relationships we selected are as follows:

II. Implementation

Next, we enter the project development phase to implement the above process diagrams one by one

1. Project environment

Start with a common SpringBoot project, select version 2.2.1.RELEASE

The pom depends on the following

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>com.github.hui.media</groupId>
        <artifactId>qrcode-plugin</artifactId>
        <version>2.2</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </pluginManagement>
</build>
<repositories>
    <repository>
        <id>spring-releases</id>
        <name>Spring Releases</name>
        <url>https://repo.spring.io/libs-release-local</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>yihui-maven-repo</id>
        <url>https://raw.githubusercontent.com/liuyueyi/maven-repository/master/repository</url>
    </repository>
</repositories>

Key Dependency Description

  • qrcode-plugin: Not me. It's probably the best, most flexible, java-side toolkit to use and support the generation of cool QR codes. Currently, version 2.2. When introducing dependencies, specify the warehouse address https://raw.githubusercontent.com/liuyueyi/maven-repository/master/repository
  • spring-boot-starter-thymeleaf: The template rendering engine we chose, which does not have front-end and back-end separation, and a project contains all the feature points

Configuration file application.yml

server:
  port: 8080

spring:
  thymeleaf:
    mode: HTML
    encoding: UTF-8
    servlet:
      content-type: text/html
    cache: false

Get native ip

Provides a tool class for getting native ip, avoiding hard-coded url s, resulting in non-versatility

import java.net.*;
import java.util.Enumeration;

public class IpUtils {
    public static final String DEFAULT_IP = "127.0.0.1";

    /**
     * Avoid returning 127.0.0.1 by directly using the first network card address as its intranet ipv4 address
     *
     * @return
     */
    public static String getLocalIpByNetcard() {
        try {
            for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements(); ) {
                NetworkInterface item = e.nextElement();
                for (InterfaceAddress address : item.getInterfaceAddresses()) {
                    if (item.isLoopback() || !item.isUp()) {
                        continue;
                    }
                    if (address.getAddress() instanceof Inet4Address) {
                        Inet4Address inet4Address = (Inet4Address) address.getAddress();
                        return inet4Address.getHostAddress();
                    }
                }
            }
            return InetAddress.getLocalHost().getHostAddress();
        } catch (SocketException | UnknownHostException e) {
            return DEFAULT_IP;
        }
    }

    private static volatile String ip;

    public static String getLocalIP() {
        if (ip == null) {
            synchronized (IpUtils.class) {
                if (ip == null) {
                    ip = getLocalIpByNetcard();
                }
            }
        }
        return ip;
    }
}

2. Login Interface

The @CrossOrigin annotation supports cross-domain because we will use localhost to access the login interface later in our tests; however, the sse registration is using native ip, so there will be cross-domain issues that may not exist in the actual project

Logic for login page, a QR code returned after accessing that contains the login authorization url

@CrossOrigin
@Controller
public class QrLoginRest {
    @Value(("${server.port}"))
    private int port;

    @GetMapping(path = "login")
    public String qr(Map<String, Object> data) throws IOException, WriterException {
        String id = UUID.randomUUID().toString();
        // IpUtils is a tool class for getting native ip. If you use 127.0.0.1 and localhost for native testing, there will be problems with app scanner access.
        String ip = IpUtils.getLocalIP();

        String pref = "http://" + ip + ":" + port + "/";
        data.put("redirect", pref + "home");
        data.put("subscribe", pref + "subscribe?id=" + id);


        String qrUrl = pref + "scan?id=" + id;
        // The following line generates a 2-D code with a width of 200, a height of red, a dot, and a base64 encoding
        // One line at a time, it's so easy, it's so safe
        String qrCode = QrCodeGenWrapper.of(qrUrl).setW(200).setDrawPreColor(Color.RED)
                .setDrawStyle(QrCodeOptions.DrawStyle.CIRCLE).asString();
        data.put("qrcode", DomUtil.toDomSrc(qrCode, MediaType.ImageJpg));
        return "login";
    }
}

Notice the implementation above, where we return a view and pass in three pieces of data

  • redirect: jump url (page that jumps after app authorization)
  • subscribe: Subscription URL (users access this url, open long connections, and receive scanners, login events pushed by the server)
  • Qrcode: 2-D code picture in Base64 format

Note: both subscribe and qrcode use globally unique id s, which are important in subsequent operations

Then the corresponding HTML page, under the resources/templates file, adds the file login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="SpringBoot thymeleaf"/>
    <meta name="author" content="YiHui"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>QR Code Interface</title>
</head>
<body>

<div>
    <div class="title">Please Scavenge Login</div>
    <img th:src="${qrcode}"/>
    <div id="state" style="display: none"></div>

    <script th:inline="javascript">
        var stateTag = document.getElementById('state');

        var subscribeUrl = [[${subscribe}]];
        var source = new EventSource(subscribeUrl);
        source.onmessage = function (event) {
            text = event.data;
            console.log("receive: " + text);
            if (text == 'scan') {
                stateTag.innerText = 'Scanned';
                stateTag.style.display = 'block';
            } else if (text.startsWith('login#')) {
                // Login format is login#cookie
                var cookie = text.substring(6);
                document.cookie = cookie;
                window.location.href = [[${redirect}]];
                source.close();
            }
        };

        source.onopen = function (evt) {
            console.log("Start Subscription");
        }
    </script>
</div>
</body>
</html>

Note the html implementation above, the label id state is invisible by default; SSE is implemented through EventSource (with the advantage of real-time and retry capabilities), and the results returned are formatted.

  • If a service side scan message is received, modify the state label copy and make it visible
  • If you receive server-side login#cookie format data, the login is successful, #followed by cookies, set local cookies, redirect to home page, and close long connections

Next in the script tag, if you need to access the passed parameters, notice the following two points

  • You need to add th:inline="javascript" to the script tag
  • [[${}]] Get delivery parameters

3. sse interface

In the previous login interface, a sse registration interface is returned, which is accessed by the client when accessing the login page. Follow our previous SSE tutorial documentation, it can be implemented as follows

private Map<String, SseEmitter> cache = new ConcurrentHashMap<>();

@GetMapping(path = "subscribe", produces = {org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter subscribe(String id) {
    // Set a five-minute timeout
    SseEmitter sseEmitter = new SseEmitter(5 * 60 * 1000L);
    cache.put(id, sseEmitter);
    sseEmitter.onTimeout(() -> cache.remove(id));
    sseEmitter.onError((e) -> cache.remove(id));
    return sseEmitter;
}

4. Scavenging interface

The next step is to scan the QR code for access to the authorization page, which is a simple logic

@GetMapping(path = "scan")
public String scan(Model model, HttpServletRequest request) throws IOException {
    String id = request.getParameter("id");
    SseEmitter sseEmitter = cache.get(request.getParameter("id"));
    if (sseEmitter != null) {
        // Tell pc that code has been scanned
        sseEmitter.send("scan");
    }

    // Authorized consent url
    String url = "http://" + IpUtils.getLocalIP() + ":" + port + "/accept?id=" + id;
    model.addAttribute("url", url);
    return "scan";
}

When a user visits this page, he or she locates the corresponding pc client based on the id passed in and sends a scan message

Authorization page is a simple implementation, just add a hyperlink of authorization, and then add user token as appropriate (as there is no separate app and user system, so as a demonstration below, a token is randomly generated to replace it)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="SpringBoot thymeleaf"/>
    <meta name="author" content="YiHui"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Scavenging login interface</title>
</head>
<body>

<div>
    <div class="title">Are you sure you want to sign in?</div>

    <div>
        <a id="login">Sign in</a>
    </div>

    <script th:inline="javascript">

        // Generate uuid to simulate passing user token
        function guid() {

            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
                var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);

            });
        }

        // Get the actual token, complete the parameters, this is just a simple simulation
        var url = [[${url}]];
        document.getElementById("login").href = url + "&token=" + guid();
    </script>

</div>
</body>
</html>

5. Authorization Interface

After clicking on the authorization hyperlink above, the login is successful. Our backend implementation is as follows

@ResponseBody
@GetMapping(path = "accept")
public String accept(String id, String token) throws IOException {
    SseEmitter sseEmitter = cache.get(id);
    if (sseEmitter != null) {
        // Send login success events and take the user's token with you. Here we use cookie s to save the token
        sseEmitter.send("login#qrlogin=" + token);
        sseEmitter.complete();
        cache.remove(id);
    }

    return "Login Successful: " + token;
}

6. Home page

Once the user authorization is successful, it will automatically jump to the first page. We can make the first page a little easier and get a welcome copy.

@GetMapping(path = {"home", ""})
@ResponseBody
public String home(HttpServletRequest request) {
    Cookie[] cookies = request.getCookies();
    if (cookies == null || cookies.length == 0) {
        return "Not logged in!";
    }

    Optional<Cookie> cookie = Stream.of(cookies).filter(s -> s.getName().equalsIgnoreCase("qrlogin")).findFirst();
    return cookie.map(cookie1 -> "Welcome to Home Page: " + cookie1.getValue()).orElse("Not logged in!");
}

7. Measurements

Now that a complete login authorization is complete and ready for a hands-on exercise, here is a complete screenshot of the demonstration (although I did not really scan for login with app, but identified the QR code address and authorized in browser, this does not actually affect the whole process, and the same is true for you to scan for authorization with QR code)

Notice a few key points in the above screenshot

  • After scanning, the scanned text will be displayed under the QR code of the login interface
  • After successful authorization, the login interface will actively jump to the home page and display the welcome xxx, noting that the users are consistent

8. Summary

The actual business development options may not be the same as those proposed in this paper, or there may be a more elegant implementation (please have some experience in this area of the Big Blacks Bulletin). This article only serves as a reference, does not represent a standard, does not represent complete accuracy, if you bring everyone into the pit, please leave a message (of course I will not be responsible for it):

The above demonstrates a sample project of bare-handed two-dimensional code login, mainly used for technical points

  • qrcode-plugin: Generate QR code, Again, Amway strongly believes that QR code generation toolkits are best used in a java ecosystem https://github.com/liuyueyi/quick-media/blob/master/plugins/qrcode-plugin (It's a bit fierce, but I don't charge for advertising because I wrote it, too)_)
  • SSE: Service-side push events, service-side single channel communication, message push
  • SpringBoot/Thymeleaf: Demonstrating project infrastructure

Finally, I think it's good to admire it. It's OK to add a friend to have a chat and pay attention to the WeChat public number to support one or two.

III. Other

0. Project

Related posts

For this blog post, some of the points of knowledge can be supplemented by the following articles

1.A grey Blog

Unlike letters, the above are purely family statements. Due to limited personal abilities, there are unavoidable omissions and errors. If bug s are found or there are better suggestions, you are welcome to criticize and correct them with gratitude.

Below is a grey personal blog, which records all the blogs in study and work. Welcome to visit it

Tags: Programming Spring QRCode Thymeleaf SpringBoot

Posted on Sun, 05 Apr 2020 00:04:40 -0700 by Will Poulson