美文网首页
spring boot 扫码登陆连接

spring boot 扫码登陆连接

作者: 做人要简单 | 来源:发表于2017-08-25 10:29 被阅读284次

    前言

    在业内,扫码登陆不是什么新技术了,我这里主要是想自己实现一下这个功能,用的是简单实现,提供的只是思路
    具体可以参考网上的其他文章
    扫码登录是如何实现的?

    开发环境

    mac+idea+paw+chrome+mysql  
    开发语言:java+kotlin
    
    mac:我的开发系统  
    idea:开发工具  
    paw:http调试工具  
    

    插一句

    开发语言使用kotlin是有原因,kotlin是构建在jvm上的,而且有很多很方便的语法糖,敲代码速度很快
    

    启动项目
    首先配置一个spring boot的项目,这里使用maven构建的方案,因为我这里使用gradle构建总是会出现各种奇怪的问题

    pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.kikt</groupId>
        <artifactId>myapp</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <!--<packaging>war</packaging>-->
        <packaging>war</packaging>
    
        <name>myapp</name>
        <description>MyApp</description>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>1.5.4.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>
            <kotlin.version>1.1.3-2</kotlin.version>
        </properties>
    
        <dependencies>
            <!--<dependency>-->
            <!--<groupId>org.springframework.boot</groupId>-->
            <!--<artifactId>spring-boot-starter-data-jpa</artifactId>-->
            <!--</dependency>-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-rest</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.0</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
                <!--<scope>provided</scope>-->
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
                <!--<scope>provided</scope>-->
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
            <!--<dependency>-->
                <!--<groupId>org.mybatis.spring.boot</groupId>-->
                <!--<artifactId>mybatis-spring-boot-starter</artifactId>-->
                <!--<version>1.3.0</version>-->
            <!--</dependency>-->
    
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
                <version>1.5.4.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-stdlib-jre8</artifactId>
                <version>${kotlin.version}</version>
            </dependency>
            <dependency>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-test</artifactId>
                <version>${kotlin.version}</version>
                <scope>test</scope>
            </dependency>
            <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.35</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.json/json -->
            <dependency>
                <groupId>org.json</groupId>
                <artifactId>json</artifactId>
                <version>20170516</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.apache.tomcat</groupId>
                        <artifactId>tomcat-jdbc</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
            </dependency>
    
            <!--<dependency>-->
            <!--<groupId>org.springframework.boot</groupId>-->
            <!--<artifactId>spring-boot-starter-ssl</artifactId>-->
            <!--</dependency>-->
    
            <!--netty-->
            <dependency>
                <groupId>io.netty</groupId>
                <artifactId>netty-all</artifactId>
                <version>4.1.13.Final</version>
            </dependency>
        </dependencies>
    
        <build>
            <finalName>myapp</finalName>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
                <plugin>
                    <groupId>org.jetbrains.kotlin</groupId>
                    <artifactId>kotlin-maven-plugin</artifactId>
                    <version>${kotlin.version}</version>
                    <executions>
                        <execution>
                            <id>compile</id>
                            <phase>compile</phase>
                            <goals>
                                <goal>compile</goal>
                            </goals>
                        </execution>
                        <execution>
                            <id>test-compile</id>
                            <phase>test-compile</phase>
                            <goals>
                                <goal>test-compile</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <jvmTarget>1.8</jvmTarget>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>compile</id>
                            <phase>compile</phase>
                            <goals>
                                <goal>compile</goal>
                            </goals>
                        </execution>
                        <execution>
                            <id>testCompile</id>
                            <phase>test-compile</phase>
                            <goals>
                                <goal>testCompile</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
        <repositories>
            <repository>
                <id>spring-milestone</id>
                <url>http://repo.spring.io/libs-release</url>
            </repository>
        </repositories>
    
    </project>
    
    

    其中有一些是其他的配置,比如netty是在内部构建一个netty服务器,注入spring进行管理

    server:
      port: 8433
      tomcat:
        uri-encoding: utf-8
    
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/app?autoReconnect=true&useUnicode=true&characterEncoding=utf-8
        username: root
        password: root
        driver-class-name: com.mysql.jdbc.Driver
    #    type: com.alibaba.druid.pool.DruidDataSource
      profiles:
        active: dev
    #    active: prod
    #    active: test
    #jpg
      jpa:
        database: mysql
        show-sql: true
        hibernate:
          ddl-auto: update
      jooq:
        sql-dialect:
    #thymeleaf
      thymeleaf:
        mode: HTML5
    
    
    #mybatis:
    #  mapperLocations: classpath:mapper/*.xml
    #  type-aliases-package: com.kikt.api.responsedata
    
    

    配置文件,使用的是yml的格式,也算比较容易理解吧

    首先配置几个Controller

    除网页外,其他所有的交互方式使用restful的方式

    @RestController
    @RequestMapping("/user")
    public class UserCtl extends BaseCtl {
    
        @Autowired
        private ScanService scanService;
    
        @Autowired
        private LoginService loginService;
    
        @RequestMapping(value = "/login/{username}", method = RequestMethod.POST)
        public String login(@PathVariable("username") String username, @RequestParam("pwd") String pwd) {
            return loginService.login(username, pwd);
        }
    
        @RequestMapping(value = "/login", method = RequestMethod.GET)
        public String index(Model model, HttpServletRequest request) {
            String sessionId = scanService.getSessionId();
            String scheme = request.getScheme();
            logger.debug("URL:" + request.getRequestURL());
            String serverName = request.getServerName();
            logger.debug("addr:" + serverName);
            String contextPath = request.getContextPath();
            logger.debug("contextPath:" + contextPath);
            int serverPort = request.getServerPort();
            logger.debug("serverport:" + serverPort);
    
            StringBuilder path = new StringBuilder();
            path.append(scheme).append("://").append(serverName).append(":").append(serverPort).append(contextPath);
    
            model.addAttribute("sessionId", sessionId);
            model.addAttribute("qrcode", path + "/user/login/" + sessionId);
            return "index";
        }
    
        //for html wait login
        @RequestMapping(value = "/wait/{sessionId}", method = RequestMethod.POST)
        @ResponseBody
        public String waitLogin(@PathVariable("sessionId") String sessionId) {
            return scanService.waitForLogin(sessionId);
        }
    
        //phone scan for the login
        @RequestMapping(value = "/login/{sessionId}", method = RequestMethod.POST)
        @ResponseBody
        public String scanWithLogin(@PathVariable("sessionId") String sessionId, @RequestParam String username, @RequestParam String token) {
            loginService.checkTokenWithName(username, token);
            return scanService.scanWithLogin(sessionId, username);
        }
    }
    

    这样就可以使用 http://localhost:8433/user/login/user 这样的url,使用post方式,模拟表单
    同一个url,使用get的方式,获取的就是二维码的显示页面
    这里index是定义到一个模板页面,model中可以设置一些属性在模板文件中进行调用,我这里模板用的是thymeleaf

    模板文件

    放在src/main/resources/templates 目录下,也就是在生成放置application.properties的目录中新建一个templates目录,在其中新建一个index.html,这样controller就会使用模板渲染html
    使用了jquery和jquery.qrcode两个js库,其中jquery是网络访问使用,qrcode依赖于jquery,同时提供qrcode的生成

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <meta name="viewport" content="width=device-width,initial-scale=1"/>
        <!--<link th:href="@{bootstrap/css/bootstrap.min.css}" rel="stylesheet"/>-->
        <!--<link th:href="@{bootstrap/css/bootstrap-theme.min.css}" rel="stylesheet"/>-->
        <meta charset="UTF-8"/>
        <script type="text/javascript" th:src="@{/js/jquery-3.2.1.min.js}"></script>
        <!--<script type="text/javascript" th:src="@{/js/qrcode.js}"></script>-->
        <script type="text/javascript" src="http://cdn.bootcss.com/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
        <!--<script type="text/javascript" src="../static/js/jquery-3.2.1.min.js"></script>-->
    
        <script th:inline="javascript" type="text/javascript">
    
            function makeQrImage(sessionId) {
                var qrcode = [[${qrcode}]]
                $('#code').qrcode(qrcode);
                $('p').text(qrcode)
    
                $.ajax({
                    url:'./wait/'+sessionId,
                    method:'post',
                    success:login,
                    fail:loginFail
                })
            }
    
            var login = function (result) {
                $('p').text(result)
            };
    
            var loginFail = function (result) {
                $('p').text(result)
            }
    
            $(document).ready(function () {
                var sessionId = [[${sessionId}]]
                makeQrImage(sessionId)
            });
        </script>
        <style>
            .qrcode {
                width: 150px;
                height: 150px;
            }
        </style>
        <title>Title</title>
    </head>
    <body>
    
    <div id="code"></div>
    <div id="result"><p></p></div>
    
    </body>
    </html>
    

    [[${sessionId}]] 就是读取model中的sessionId属性

    同理
    [[${qrcode}]]就是model中的qrcode属性

    这里 <script th:inline="javascript" type="text/javascript"></script>
    的标签中使用了th:inline="javascript" 这样的写法,这个就是模板的写法了,让js标签内可以识别模板中的变量等等

    这里ajax中使用了硬编码,可以考虑使用java中的model传过来,如同qrcode的url一样,这样就可以在不动html的情况下,完成后台url的切换

    这里其实逻辑比较简单

    步骤

    前端网页: 访问 /user/login GET方式,提示扫码,然后使用已经登录的手机扫码,同时创建一个ajax连接,后台hold住此链接等待扫码,使用的是长轮询的方案

    手机端:访问/user/login/adminPOST方式,先登录,获取了token和username,然后再使用扫码,传入参数username,token

    mysql数据库表设计(相关逻辑)

    用户表
    记录用户相关的数据,包括id,用户名,email,注册时间等信息
    
    登录token表
    记录用户token,和token更新时间,token信息
    

    具体的java端实现
    上面只是简单的流程步骤,具体的实现还是需要到service中去看

    package com.kikt.api.service.scan
    
    import com.kikt.api.exeption.ErrorEnum
    import com.kikt.api.ext.toJson
    import com.kikt.api.service.BaseService
    import com.kikt.api.service.user.LoginService
    import com.kikt.response.Response
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.stereotype.Service
    import java.util.*
    import java.util.concurrent.*
    
    /**
     * Created by cai on 2017/8/24.
     */
    @Service
    open class ScanService : BaseService {
    
        @Autowired
        private var loginService: LoginService? = null
    
        private val map: MutableMap<String, LoginSession> = mutableMapOf()
    
        fun getSessionId(): String {
            val random = UUID.randomUUID().toString()
            map.put(random, LoginSession())
            return random
        }
    
        fun waitForLogin(sessionId: String): String {
            val sessionData = map[sessionId] ?: ErrorEnum.SESSION_SCAN_TIME_OUT.throwError()
            val waitForLogin: String
            try {
                waitForLogin = sessionData.waitForLogin()
            } catch(e: Exception) {
                map.remove(sessionId)
                ErrorEnum.SESSION_SCAN_TIME_OUT.throwError()
            }
            map.remove(sessionId)
            return waitForLogin
        }
    
        fun scanWithLogin(sessionId: String, username: String): String {
            val sessionData = map[sessionId] ?: ErrorEnum.SESSION_NO_FOUNT.throwError()
            val result = loginService?.login(username, 2)
            if (result != null) {
                sessionData.login(result)
            }
            return Response.newSuccessResponse("成功").toJson()
        }
    
    }
    
    class LoginSession {
    
        private val queue: BlockingQueue<String> = LinkedBlockingQueue(2)
    
        companion object {
            val TIME_OUT: Long = 60000
    
            val threadPool: ExecutorService = Executors.newFixedThreadPool(30)
        }
    
        fun waitForLogin(): String {
            val take: String?
            try {
                runDelayTimeout()
                take = queue.take()
            } catch(e: InterruptedException) {
                throw e
            }
            return take
        }
    
        fun login(result: String) {
            queue.offer(result)
        }
    
        fun runDelayTimeout() {
            val currentThread = Thread.currentThread()
            threadPool.execute {
                Thread.sleep(TIME_OUT)
                currentThread.interrupt()
            }
        }
    }
    

    总体思路是:定义一个map用于记录sesstionId,和具体的LoginSession

    LoginSession中包含一个阻塞队列,在index的ctl中创建sessionId和loginSession对象,在访问/wait/sessionId时调用,等待扫码,称为连接1

    扫码时,创建连接2,根据token检验手机登陆用户,然后根据sessionId找到LoginSession对象,给队列传入数据,这样LoginSession.take()返回后结果后,连接1返回登陆信息,同时登陆2返回成功的信息

    优化

    上面的连接1中需要设置一个超时时间,超时后返回失败,这里创建一个线程池,30秒后尝试中断线程,上面

        fun runDelayTimeout() {
            val currentThread = Thread.currentThread()
            threadPool.execute {
                Thread.sleep(TIME_OUT)
                currentThread.interrupt()
            }
        }
    

    执行60秒后过时,连接1返回失败信息,前端根据失败信息显示刷新重试的样式即可

    后记

    总体思路和主要代码都放出来了,具体的实现应该还有更优解,这里我就不尝试了,只起到思路引领

    相关文章

      网友评论

          本文标题:spring boot 扫码登陆连接

          本文链接:https://www.haomeiwen.com/subject/azoydxtx.html