spring boot 扫码登陆连接

前言

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

开发环境

mac+idea+paw+chrome+mysql  
开发语言:java+kotlin

mac:我的开发系统  
idea:开发工具  
paw:http调试工具  

插一句

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

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

pom.xml



    4.0.0

    com.kikt
    myapp
    0.0.1-SNAPSHOT
    
    war

    myapp
    MyApp

    
        org.springframework.boot
        spring-boot-starter-parent
        1.5.4.RELEASE
         
    

    
        UTF-8
        UTF-8
        1.8
        1.1.3-2
    

    
        
        
        
        
        
            org.springframework.boot
            spring-boot-starter-data-rest
        
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            mysql
            mysql-connector-java
            runtime
        

        
            com.alibaba
            druid
            1.1.0
        

        
            org.springframework.boot
            spring-boot-starter-tomcat
            
        
        
            org.springframework.boot
            spring-boot-starter-actuator
            
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
        
            
            
            
        


        
            org.springframework.boot
            spring-boot-starter-aop
        

        
            org.springframework.boot
            spring-boot-starter-data-jpa
            1.5.4.RELEASE
        

        
            org.jetbrains.kotlin
            kotlin-stdlib-jre8
            ${kotlin.version}
        
        
            org.jetbrains.kotlin
            kotlin-test
            ${kotlin.version}
            test
        
        
        
            com.alibaba
            fastjson
            1.2.35
        

        
        
            org.json
            json
            20170516
        

        
            org.springframework.boot
            spring-boot-starter-jdbc
            
                
                    org.apache.tomcat
                    tomcat-jdbc
                
            
        

        
            org.springframework.boot
            spring-boot-starter-thymeleaf
        
        
            org.springframework.boot
            spring-boot-devtools
        

        
        
        
        

        
        
            io.netty
            netty-all
            4.1.13.Final
        
    

    
        myapp
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
            
                org.jetbrains.kotlin
                kotlin-maven-plugin
                ${kotlin.version}
                
                    
                        compile
                        compile
                        
                            compile
                        
                    
                    
                        test-compile
                        test-compile
                        
                            test-compile
                        
                    
                
                
                    1.8
                
            
            
                org.apache.maven.plugins
                maven-compiler-plugin
                
                    
                        compile
                        compile
                        
                            compile
                        
                    
                    
                        testCompile
                        test-compile
                        
                            testCompile
                        
                    
                
            
        
    
    
        
            spring-milestone
            http://repo.spring.io/libs-release
        
    



其中有一些是其他的配置,比如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的生成




    
    
    
    
    
    
    
    

    
    
    Title



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

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

这里
的标签中使用了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 = 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 = 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 扫码登陆连接)