Hand – to – hand sweep a code login example project

I don’t know if it is wechat, now there are more and more scenes of scanning code login, as a new four good code farmers with pursuit and ideal, of course, have to keep up with the trend of The Times, have to hand off a example

This sample project mainly uses the following technology stack

  • qrcode-plugin: Open source QR code generation toolkit, project link:Github.com/liuyueyi/qu…
  • SpringBoot: Basic project environment
  • thymeleaf: page rendering engine
  • SSE/ asynchronous request: Event pushed by the server
  • js: Basic operations of native JS

I. Principle analysis

According to the previous plan, should give priority to write the file download related blog, however, saw a blog about the principle of scanning code login, found that it can be combined with the previous asynchronous request /SSE, do a practical application, so there is this blog

For the principle of scanning code login, please check: chat about the principle of two-dimensional code scanning login

1. Scenario description

In order to take care of the scanning code login may not understand the students, here is a simple introduction to what it is

Generally speaking, scan code login, involving both ends, three steps

  • On the PC, you can log in to a website. The login method of this website is different from the traditional user name/password (mobile phone number/verification code), but a TWO-DIMENSIONAL code is displayed
  • App side, use the app of this website, first make sure you are logged in, then scan the TWO-DIMENSIONAL code, pop up a page of login authorization, click Authorization
  • If the login succeeds, the home page is automatically displayed

2. Brief introduction of principle and process

In the design of the whole system, the most core point is the mobile phone terminal scan code, PC login success, what is the principle of this?

  • We assume that app and backend are identified by token
  • App scans code for authorization and transfers token to the back end. The back end can determine who initiates the login request on the PC side according to the token
  • The backend will write the successful login status back to the PC requester and jump to the home page (this is equivalent to the normal process after the successful login of the user, you can choose session, cookie or JWT).

With the help of the above principle, step by step analysis of key points

  • Log in to the PC and generate a QR code
    • The TWO-DIMENSIONAL code must be unique, and bind the identity of the requestor (otherwise assume that the two-dimensional code of two people is the same, one person scan the code to log in, the other one is also logged in?)
    • The client keeps the connection with the server, so as to receive the subsequent login success and adjust the home page event (there are many options, such as polling, long connection push).
  • App scans code and authorizes login
    • After scanning the code, jump to the authorization page (so the two-dimensional code should be a URL)
    • Authorization (identification, binding of identity information to THE PC requestor, and redirecting to the home page)

Finally, we selected the business process relationship as follows:

II. The implementation

Then enter the project development stage, for the above flow chart one by one implementation

1. Project environment

First of all, there is a common SpringBoot project. Select version 2.2.1.release

Pom dependencies are as follows

<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>
Copy the code

Key dependency description

  • qrcode-plugin: Not to be bluster, this is probably the most useful, flexible, and cool QR code generation toolkit on the Java side, the latest version2.2When importing dependencies, specify the repository addresshttps://raw.githubusercontent.com/liuyueyi/maven-repository/master/repository
  • spring-boot-starter-thymeleaf: The template rendering engine we chose, where there is no front and back end separation, contains all the function points in one project

The configuration fileapplication.yml

server:
  port: 8080

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

Obtain the local IP address

Provide a utility class to get the native IP, avoid hard-coded urls, resulting in ungeneric

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

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

    /** * Uses the address of the first nic as the Intranet ipv4 address instead of returning 127.0.0.1 **@return* /
    public static String getLocalIpByNetcard(a) {
        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();
                        returninet4Address.getHostAddress(); }}}return InetAddress.getLocalHost().getHostAddress();
        } catch (SocketException | UnknownHostException e) {
            returnDEFAULT_IP; }}private static volatile String ip;

    public static String getLocalIP(a) {
        if (ip == null) {
            synchronized (IpUtils.class) {
                if (ip == null) { ip = getLocalIpByNetcard(); }}}returnip; }}Copy the code

2. Login interface

The @crossorigin annotation supports cross-domain, since we will use localhost to access the login interface later in the test; But SSE registrations use native IP, so there are cross-domain issues that may not exist in real projects

Login page logic, a two-dimensional code returned after access, two-dimensional code content is 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 to obtain the local IP address. If you use 127.0.0.1, localhost in the local test, then the app will have a problem accessing the code
        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 200-by-200 red, dotted QR code encoded in Base64
        // One line, simple and easy, strong Amway
        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"; }}Copy the code

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

  • Redirect: Redirect to the url (after the app is authorized)
  • Subscribe: a url that the user accesses, opens a long connection, and receives the scan and login events pushed by the server.
  • Qrcode: qrcode image in base64 format

Note:subscribeandqrcodeGlobally unique IDS are used, and this parameter will be important in future operations

Add the file login. HTML to the corresponding HTML page in the Resources /templates file

<! DOCTYPEhtml>
<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>Two-dimensional code interface</title>
</head>
<body>

<div>
    <div class="title">Please scan code to log in</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#')) {
                // the 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 subscribing");
        }
    </script>
</div>
</body>
</html>
Copy the code

Note that in the HTML implementation above, the tag id state is not visible by default; SSE is implemented through EventSource (with the advantage of real-time and retry capability) and formatted for the returned results

  • If received from the serverscanMessage, modify the state tag text and set it to visible
  • If received from the serverlogin#cookieFormat data, indicating that the login is successful.#Set the local cookie, then redirect to the home page, and close the long connection

Second, in the script tag, if you need access to passed parameters, note the following two points

  • Need to be added on the script tagth:inline="javascript"
  • [[${}]]Get transfer parameters

3. The sse interface

In the previous login interface, a SSE registration interface is returned. The client will access this interface when accessing the login page. According to our previous SSE tutorial document, this interface 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;
}
Copy the code

4. Scan interface

Next is to scan the TWO-DIMENSIONAL code into the authorization page interface, this logic is relatively simple

@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 the PC that the code has been scanned
        sseEmitter.send("scan");
    }

    // The authorized URL
    String url = "http://" + IpUtils.getLocalIP() + ":" + port + "/accept? id=" + id;
    model.addAttribute("url", url);
    return "scan";
}
Copy the code

After scanning codes to access the page, the user locates the PC client based on the transmitted ID and sends scan information

The authorization page is easier to implement, adding an authorization hyperlink is good, and then adding user tokens according to the actual situation (since there is no independent APP and user system, the following is a demonstration to randomly generate a token to replace it).

<! DOCTYPEhtml>
<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>Scan the login page</title>
</head>
<body>

<div>
    <div class="title">Are you sure you're logged in?</div>

    <div>
        <a id="login">The login</a>
    </div>

    <script th:inline="javascript">

        // Generate a UUID to simulate the transfer of the 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 parameter, here is a simple simulation
        var url = [[${url}]];
        document.getElementById("login").href = url + "&token=" + guid();
    </script>

</div>
</body>
</html>
Copy the code

5. Authorize interfaces

After clicking the authorization hyperlink above, the login is successful. Our back-end 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 a successful login event with the user's token. We use cookies to save the token here
        sseEmitter.send("login#qrlogin=" + token);
        sseEmitter.complete();
        cache.remove(id);
    }

    return "Login successful:" + token;
}
Copy the code

6. The home page

After the user authorization is successful, it will automatically jump to the home page. We can simply make a welcome copy on the home page

@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!");
}
Copy the code

7. The measured

Login to this a complete authorization is done, can the practical exercise, is a complete demo screenshot below (although I didn’t really use app to scan log in, but the recognition of qr code address, in the browser for authorization, actually does not affect the whole process, you use 2 d scan authorization effect is the same)

Notice a few key points in the screenshot above

  • After scanning, the qr code will be displayed on the login pagescannedThe copy of
  • After the authorization is successful, the login page will jump to the home page and show “welcome XXX”. Note that the users are the same

Nodule 8.

Real business development scheme selection may and proposed in this paper is not quite same, there may be a more elegant way (please have the experience of bosses sermons), just as a reference, this paper does not represent the standard, not entirely accurate, if the people into the pit, please leave a message (of course I am not responsible for 🙃)

The above demonstrates the example of a two-dimensional code login by hand, mainly using the following technical points

  • qrcode-plugin: Generate two-dimensional code, once again strong amway a private Java ecosystem best use two-dimensional code generation toolkitGithub.com/liuyueyi/qu…(Although the boast is fierce, but I did not charge for advertising, because this is also written by me 😂)
  • SSE: The server pushes events. The server communicates with each other in a single channel to achieve message push
  • SpringBoot/Thymeleaf: Demonstrate the basic project environment

Finally, feel good can like it, add a friend something nothing to talk about, pay attention to a wechat public number to support one or two, it is ok

III. The other

0. Project

Related blog

As for this blog post, some knowledge points can be completed by referring to the following articles

  • 【SpringBoot WEB series 】SSE server send event details
  • 【SpringBoot WEB series 】 Asynchronous request knowledge and use posture summary
  • 【SpringBoot WEB series 】Thymeleaf environment setup

  • Project: github.com/liuyueyi/sp…
  • Project source: github.com/liuyueyi/sp…

1. An ashy Blog

As far as the letter is not as good, the above content is purely one’s opinion, due to the limited personal ability, it is inevitable that there are omissions and mistakes, if you find bugs or have better suggestions, welcome criticism and correction, don’t hesitate to appreciate

Below a gray personal blog, record all the study and work of the blog, welcome everyone to go to stroll

  • A grey Blog Personal Blog blog.hhui.top
  • A Grey Blog-Spring feature Blog Spring.hhui.top