spring boot 扫码登录 后端实现
文章目录
前言
在业内,扫码登陆不是什么新技术了,我这里主要是想自己实现一下这个功能,用的是简单实现,提供的只是思路 具体可以参考网上的其他文章 扫码登录是如何实现的?
开发环境
mac+idea+paw+chrome+mysql
开发语言:java+kotlin
mac:我的开发系统
idea:开发工具
paw:http调试工具
插一句
开发语言使用kotlin是有原因,kotlin是构建在jvm上的,而且有很多很方便的语法糖,敲代码速度很快
启动项目
首先配置一个 spring boot 的项目,这里使用 maven 构建的方案,因为我这里使用 gradle 构建总是会出现各种奇怪的问题
pom.xml
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4 <modelVersion>4.0.0</modelVersion>
5
6 <groupId>com.kikt</groupId>
7 <artifactId>myapp</artifactId>
8 <version>0.0.1-SNAPSHOT</version>
9 <!--<packaging>war</packaging>-->
10 <packaging>war</packaging>
11
12 <name>myapp</name>
13 <description>MyApp</description>
14
15 <parent>
16 <groupId>org.springframework.boot</groupId>
17 <artifactId>spring-boot-starter-parent</artifactId>
18 <version>1.5.4.RELEASE</version>
19 <relativePath/> <!-- lookup parent from repository -->
20 </parent>
21
22 <properties>
23 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
24 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
25 <java.version>1.8</java.version>
26 <kotlin.version>1.1.3-2</kotlin.version>
27 </properties>
28
29 <dependencies>
30 <!--<dependency>-->
31 <!--<groupId>org.springframework.boot</groupId>-->
32 <!--<artifactId>spring-boot-starter-data-jpa</artifactId>-->
33 <!--</dependency>-->
34 <dependency>
35 <groupId>org.springframework.boot</groupId>
36 <artifactId>spring-boot-starter-data-rest</artifactId>
37 </dependency>
38 <dependency>
39 <groupId>org.springframework.boot</groupId>
40 <artifactId>spring-boot-starter-web</artifactId>
41 </dependency>
42
43 <dependency>
44 <groupId>mysql</groupId>
45 <artifactId>mysql-connector-java</artifactId>
46 <scope>runtime</scope>
47 </dependency>
48
49 <dependency>
50 <groupId>com.alibaba</groupId>
51 <artifactId>druid</artifactId>
52 <version>1.1.0</version>
53 </dependency>
54
55 <dependency>
56 <groupId>org.springframework.boot</groupId>
57 <artifactId>spring-boot-starter-tomcat</artifactId>
58 <!--<scope>provided</scope>-->
59 </dependency>
60 <dependency>
61 <groupId>org.springframework.boot</groupId>
62 <artifactId>spring-boot-starter-actuator</artifactId>
63 <!--<scope>provided</scope>-->
64 </dependency>
65 <dependency>
66 <groupId>org.springframework.boot</groupId>
67 <artifactId>spring-boot-starter-test</artifactId>
68 <scope>test</scope>
69 </dependency>
70
71 <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
72 <!--<dependency>-->
73 <!--<groupId>org.mybatis.spring.boot</groupId>-->
74 <!--<artifactId>mybatis-spring-boot-starter</artifactId>-->
75 <!--<version>1.3.0</version>-->
76 <!--</dependency>-->
77
78
79 <dependency>
80 <groupId>org.springframework.boot</groupId>
81 <artifactId>spring-boot-starter-aop</artifactId>
82 </dependency>
83
84 <dependency>
85 <groupId>org.springframework.boot</groupId>
86 <artifactId>spring-boot-starter-data-jpa</artifactId>
87 <version>1.5.4.RELEASE</version>
88 </dependency>
89
90 <dependency>
91 <groupId>org.jetbrains.kotlin</groupId>
92 <artifactId>kotlin-stdlib-jre8</artifactId>
93 <version>${kotlin.version}</version>
94 </dependency>
95 <dependency>
96 <groupId>org.jetbrains.kotlin</groupId>
97 <artifactId>kotlin-test</artifactId>
98 <version>${kotlin.version}</version>
99 <scope>test</scope>
100 </dependency>
101 <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
102 <dependency>
103 <groupId>com.alibaba</groupId>
104 <artifactId>fastjson</artifactId>
105 <version>1.2.35</version>
106 </dependency>
107
108 <!-- https://mvnrepository.com/artifact/org.json/json -->
109 <dependency>
110 <groupId>org.json</groupId>
111 <artifactId>json</artifactId>
112 <version>20170516</version>
113 </dependency>
114
115 <dependency>
116 <groupId>org.springframework.boot</groupId>
117 <artifactId>spring-boot-starter-jdbc</artifactId>
118 <exclusions>
119 <exclusion>
120 <groupId>org.apache.tomcat</groupId>
121 <artifactId>tomcat-jdbc</artifactId>
122 </exclusion>
123 </exclusions>
124 </dependency>
125
126 <dependency>
127 <groupId>org.springframework.boot</groupId>
128 <artifactId>spring-boot-starter-thymeleaf</artifactId>
129 </dependency>
130 <dependency>
131 <groupId>org.springframework.boot</groupId>
132 <artifactId>spring-boot-devtools</artifactId>
133 </dependency>
134
135 <!--<dependency>-->
136 <!--<groupId>org.springframework.boot</groupId>-->
137 <!--<artifactId>spring-boot-starter-ssl</artifactId>-->
138 <!--</dependency>-->
139
140 <!--netty-->
141 <dependency>
142 <groupId>io.netty</groupId>
143 <artifactId>netty-all</artifactId>
144 <version>4.1.13.Final</version>
145 </dependency>
146 </dependencies>
147
148 <build>
149 <finalName>myapp</finalName>
150 <plugins>
151 <plugin>
152 <groupId>org.springframework.boot</groupId>
153 <artifactId>spring-boot-maven-plugin</artifactId>
154 </plugin>
155 <plugin>
156 <groupId>org.jetbrains.kotlin</groupId>
157 <artifactId>kotlin-maven-plugin</artifactId>
158 <version>${kotlin.version}</version>
159 <executions>
160 <execution>
161 <id>compile</id>
162 <phase>compile</phase>
163 <goals>
164 <goal>compile</goal>
165 </goals>
166 </execution>
167 <execution>
168 <id>test-compile</id>
169 <phase>test-compile</phase>
170 <goals>
171 <goal>test-compile</goal>
172 </goals>
173 </execution>
174 </executions>
175 <configuration>
176 <jvmTarget>1.8</jvmTarget>
177 </configuration>
178 </plugin>
179 <plugin>
180 <groupId>org.apache.maven.plugins</groupId>
181 <artifactId>maven-compiler-plugin</artifactId>
182 <executions>
183 <execution>
184 <id>compile</id>
185 <phase>compile</phase>
186 <goals>
187 <goal>compile</goal>
188 </goals>
189 </execution>
190 <execution>
191 <id>testCompile</id>
192 <phase>test-compile</phase>
193 <goals>
194 <goal>testCompile</goal>
195 </goals>
196 </execution>
197 </executions>
198 </plugin>
199 </plugins>
200 </build>
201 <repositories>
202 <repository>
203 <id>spring-milestone</id>
204 <url>http://repo.spring.io/libs-release</url>
205 </repository>
206 </repositories>
207
208</project>
其中有一些是其他的配置,比如 netty 是在内部构建一个 netty 服务器,注入 spring 进行管理
1server:
2 port: 8433
3 tomcat:
4 uri-encoding: utf-8
5
6spring:
7 datasource:
8 url: jdbc:mysql://localhost:3306/app?autoReconnect=true&useUnicode=true&characterEncoding=utf-8
9 username: root
10 password: root
11 driver-class-name: com.mysql.jdbc.Driver
12 # type: com.alibaba.druid.pool.DruidDataSource
13 profiles:
14 active: dev
15 # active: prod
16 # active: test
17 #jpg
18 jpa:
19 database: mysql
20 show-sql: true
21 hibernate:
22 ddl-auto: update
23 jooq:
24 sql-dialect:
25 #thymeleaf
26 thymeleaf:
27 mode: HTML5
28#mybatis:
29# mapperLocations: classpath:mapper/*.xml
30# type-aliases-package: com.kikt.api.responsedata
配置文件,使用的是 yml 的格式,也算比较容易理解吧
首先配置几个 Controller
除网页外,其他所有的交互方式使用 restful 的方式
1@RestController
2@RequestMapping("/user")
3public class UserCtl extends BaseCtl {
4
5 @Autowired
6 private ScanService scanService;
7
8 @Autowired
9 private LoginService loginService;
10
11 @RequestMapping(value = "/login/{username}", method = RequestMethod.POST)
12 public String login(@PathVariable("username") String username, @RequestParam("pwd") String pwd) {
13 return loginService.login(username, pwd);
14 }
15
16 @RequestMapping(value = "/login", method = RequestMethod.GET)
17 public String index(Model model, HttpServletRequest request) {
18 String sessionId = scanService.getSessionId();
19 String scheme = request.getScheme();
20 logger.debug("URL:" + request.getRequestURL());
21 String serverName = request.getServerName();
22 logger.debug("addr:" + serverName);
23 String contextPath = request.getContextPath();
24 logger.debug("contextPath:" + contextPath);
25 int serverPort = request.getServerPort();
26 logger.debug("serverport:" + serverPort);
27
28 StringBuilder path = new StringBuilder();
29 path.append(scheme).append("://").append(serverName).append(":").append(serverPort).append(contextPath);
30
31 model.addAttribute("sessionId", sessionId);
32 model.addAttribute("qrcode", path + "/user/login/" + sessionId);
33 return "index";
34 }
35
36 //for html wait login
37 @RequestMapping(value = "/wait/{sessionId}", method = RequestMethod.POST)
38 @ResponseBody
39 public String waitLogin(@PathVariable("sessionId") String sessionId) {
40 return scanService.waitForLogin(sessionId);
41 }
42
43 //phone scan for the login
44 @RequestMapping(value = "/login/{sessionId}", method = RequestMethod.POST)
45 @ResponseBody
46 public String scanWithLogin(@PathVariable("sessionId") String sessionId, @RequestParam String username, @RequestParam String token) {
47 loginService.checkTokenWithName(username, token);
48 return scanService.scanWithLogin(sessionId, username);
49 }
50}
这样就可以使用 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 的生成
1<!DOCTYPE html>
2<html
3 xmlns:th="http://www.thymeleaf.org"
4 xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
5>
6 <head>
7 <meta name="viewport" content="width=device-width,initial-scale=1" />
8 <!--<link th:href="@{bootstrap/css/bootstrap.min.css}" rel="stylesheet"/>-->
9 <!--<link th:href="@{bootstrap/css/bootstrap-theme.min.css}" rel="stylesheet"/>-->
10 <meta charset="UTF-8" />
11 <script type="text/javascript" th:src="@{/js/jquery-3.2.1.min.js}"></script>
12 <!--<script type="text/javascript" th:src="@{/js/qrcode.js}"></script>-->
13 <script
14 type="text/javascript"
15 src="http://cdn.bootcss.com/jquery.qrcode/1.0/jquery.qrcode.min.js"
16 ></script>
17 <!--<script type="text/javascript" src="../static/js/jquery-3.2.1.min.js"></script>-->
18
19 <script th:inline="javascript" type="text/javascript">
20
21 function makeQrImage(sessionId) {
22 var qrcode = [[${qrcode}]]
23 $('#code').qrcode(qrcode);
24 $('p').text(qrcode)
25
26 $.ajax({
27 url:'./wait/'+sessionId,
28 method:'post',
29 success:login,
30 fail:loginFail
31 })
32 }
33
34 var login = function (result) {
35 $('p').text(result)
36 };
37
38 var loginFail = function (result) {
39 $('p').text(result)
40 }
41
42 $(document).ready(function () {
43 var sessionId = [[${sessionId}]]
44 makeQrImage(sessionId)
45 });
46 </script>
47 <style>
48 .qrcode {
49 width: 150px;
50 height: 150px;
51 }
52 </style>
53 <title>Title</title>
54 </head>
55 <body>
56 <div id="code"></div>
57 <div id="result"><p></p></div>
58 </body>
59</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/admin
POST 方式,先登录,获取了 token 和 username,然后再使用扫码,传入参数 username,token
mysql 数据库表设计(相关逻辑)
用户表
记录用户相关的数据,包括id,用户名,email,注册时间等信息
登录token表
记录用户token,和token更新时间,token信息
具体的 java 端实现
上面只是简单的流程步骤,具体的实现还是需要到 service 中去看
1package com.kikt.api.service.scan
2
3import com.kikt.api.exeption.ErrorEnum
4import com.kikt.api.ext.toJson
5import com.kikt.api.service.BaseService
6import com.kikt.api.service.user.LoginService
7import com.kikt.response.Response
8import org.springframework.beans.factory.annotation.Autowired
9import org.springframework.stereotype.Service
10import java.util.*
11import java.util.concurrent.*
12
13/**
14 * Created by cai on 2017/8/24.
15 */
16@Service
17open class ScanService : BaseService {
18
19 @Autowired
20 private var loginService: LoginService? = null
21
22 private val map: MutableMap<String, LoginSession> = mutableMapOf()
23
24 fun getSessionId(): String {
25 val random = UUID.randomUUID().toString()
26 map.put(random, LoginSession())
27 return random
28 }
29
30 fun waitForLogin(sessionId: String): String {
31 val sessionData = map[sessionId] ?: ErrorEnum.SESSION_SCAN_TIME_OUT.throwError()
32 val waitForLogin: String
33 try {
34 waitForLogin = sessionData.waitForLogin()
35 } catch(e: Exception) {
36 map.remove(sessionId)
37 ErrorEnum.SESSION_SCAN_TIME_OUT.throwError()
38 }
39 map.remove(sessionId)
40 return waitForLogin
41 }
42
43 fun scanWithLogin(sessionId: String, username: String): String {
44 val sessionData = map[sessionId] ?: ErrorEnum.SESSION_NO_FOUNT.throwError()
45 val result = loginService?.login(username, 2)
46 if (result != null) {
47 sessionData.login(result)
48 }
49 return Response.newSuccessResponse("成功").toJson()
50 }
51
52}
53
54class LoginSession {
55
56 private val queue: BlockingQueue<String> = LinkedBlockingQueue(2)
57
58 companion object {
59 val TIME_OUT: Long = 60000
60
61 val threadPool: ExecutorService = Executors.newFixedThreadPool(30)
62 }
63
64 fun waitForLogin(): String {
65 val take: String?
66 try {
67 runDelayTimeout()
68 take = queue.take()
69 } catch(e: InterruptedException) {
70 throw e
71 }
72 return take
73 }
74
75 fun login(result: String) {
76 queue.offer(result)
77 }
78
79 fun runDelayTimeout() {
80 val currentThread = Thread.currentThread()
81 threadPool.execute {
82 Thread.sleep(TIME_OUT)
83 currentThread.interrupt()
84 }
85 }
86}
总体思路是:定义一个 map 用于记录 sesstionId,和具体的 LoginSession
LoginSession 中包含一个阻塞队列,在 index 的 ctl 中创建 sessionId 和 loginSession 对象,在访问/wait/sessionId 时调用,等待扫码,称为连接 1
扫码时,创建连接 2,根据 token 检验手机登陆用户,然后根据 sessionId 找到 LoginSession 对象,给队列传入数据,这样 LoginSession.take()返回后结果后,连接 1 返回登陆信息,同时登陆 2 返回成功的信息
优化
上面的连接 1 中需要设置一个超时时间,超时后返回失败,这里创建一个线程池,30 秒后尝试中断线程,上面
1 fun runDelayTimeout() {
2 val currentThread = Thread.currentThread()
3 threadPool.execute {
4 Thread.sleep(TIME_OUT)
5 currentThread.interrupt()
6 }
7 }
执行 60 秒后过时,连接 1 返回失败信息,前端根据失败信息显示刷新重试的样式即可
后记
总体思路和主要代码都放出来了,具体的实现应该还有更优解,这里我就不尝试了,只起到思路引领