spring boot 自动跳转登录页面_徒手撸一个扫码登录示例工程

a61390f612506384b8afdf28e3f7a26c.png
徒手撸一个扫码登录示例工程

不知道是不是微信的原因,现在出现扫码登录的场景越来越多了,作为一个有追求、有理想新四好码农,当然得紧跟时代的潮流,得徒手撸一个以儆效尤

本篇示例工程,主要用到以下技术栈

  • qrcode-plugin:开源二维码生成工具包,项目链接: https://github.com/liuyueyi/quick-media[1]
  • SpringBoot:项目基本环境
  • thymeleaf:页面渲染引擎
  • SSE/异步请求:服务端推送事件
  • js: 原生 js 的基本操作

I. 原理解析

按照之前的计划,应该优先写文件下载相关的博文,然而看到了一篇说扫码登录原理的博文,发现正好可以和前面的异步请求/SSE 结合起来,搞一个应用实战,所以就有了本篇博文
关于扫码登录的原理,请查看: 聊一聊二维码扫描登录原理[2]

1. 场景描述

为了照顾可能对扫码登录不太了解的同学,这里简单的介绍一下它到底是个啥

一般来说,扫码登录,涉及两端,三个步骤

  • pc 端,登录某个网站,这个网站的登录方式和传统的用户名/密码(手机号/验证码)不一样,显示的是一个二维码
  • app 端,用这个网站的 app,首先确保你是登录的状态,然后扫描二维码,弹出一个登录授权的页面,点击授权
  • pc 端登录成功,自动跳转到首页

2. 原理与流程简述

整个系统的设计中,最核心的一点就是手机端扫码之后,pc 登录成功,这个是什么原理呢?

  • 我们假定 app 与后端通过 token 进行身份标识
  • app 扫码授权,并传递 token 给后端,后端根据 token 可以确定是谁在 pc 端发起登录请求
  • 后端将登录成功状态写回给 pc 请求者并跳转首页(这里相当于一般的用户登录成功之后的流程,可以选择 session、cookie 或者 jwt)

借助上面的原理,进行逐步的要点分析

  • pc 登录,生成二维码
    • 二维码要求唯一,并绑定请求端身份(否则假定两个人的二维码一致,一个人扫码登录了,另外一个岂不是也登录了?)
    • 客户端与服务端保持连接,以便收到后续的登录成功并调首页的事件(可以选择方案比较多,如轮询,长连接推送)
  • app 扫码,授权登录
    • 扫码之后,跳转授权页面(所以二维码对应的应该是一个 url)
    • 授权(身份确定,将身份信息与 pc 请求端绑定,并跳转首页)

最终我们选定的业务流程关系如下图:

43d360e6b630db1bcf4e1b9f571aa436.png

II. 实现

接下来进入项目开发阶段,针对上面的流程图进行逐一的实现

1. 项目环境

首先常见一个 SpringBoot 工程项目,选择版本2.2.1.RELEASE

pom 依赖如下

<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>

关键依赖说明

  • qrcode-plugin: 不是我吹,这可能是 java 端最好用、最灵活、还支持生成各种酷炫二维码的工具包,目前最新版本2.2,在引入依赖的时候,请指定仓库地址https://raw.githubusercontent.com/liuyueyi/maven-repository/master/repository
  • spring-boot-starter-thymeleaf: 我们选择的模板渲染引擎,这里并没有采用前后端分离,一个项目包含所有的功能点

配置文件application.yml

server:port: 8080spring:thymeleaf:mode: HTMLencoding: UTF-8servlet:content-type: text/htmlcache: false

获取本机 ip

提供一个获取本机 ip 的工具类,避免硬编码 url,导致不通用

import java.net.*;
import java.util.Enumeration;public class IpUtils {public static final String DEFAULT_IP = "127.0.0.1";/*** 直接根据第一个网卡地址作为其内网ipv4地址,避免返回 127.0.0.1** @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. 登录接口

@CrossOrigin注解来支持跨域,因为后续我们测试的时候用localhost来访问登录界面;但是 sse 注册是用的本机 ip,所以会有跨域问题,实际的项目中可能并不存在这个问题

登录页逻辑,访问之后返回的一张二维码,二维码内容为登录授权 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 为获取本机ip的工具类,本机测试时,如果用127.0.0.1, localhost那么app扫码访问会有问题哦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;// 下面这一行生成一张宽高200,红色,圆点的二维码,并base64编码// 一行完成,就这么简单省事,强烈安利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";}
}

请注意上面的实现,我们返回的是一个视图,并传递了三个数据

  • redirect: 跳转 url(app 授权之后,跳转的页面)
  • subscribe: 订阅 url(用户会访问这个 url,开启长连接,接收服务端推送的扫码、登录事件)
  • qrcode: base64 格式的二维码图片

注意:subscribeqrcode都用到了全局唯一 id,后面的操作中,这个参数很重要

接着时候对应的 html 页面,在resources/templates文件下,新增文件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>二维码界面</title>
</head>
<body><div><div class="title">请扫码登录</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 = '已扫描';stateTag.style.display = 'block';} else if (text.startsWith('login#')) {// 登录格式为 login#cookievar cookie = text.substring(6);document.cookie = cookie;window.location.href = [[${redirect}]];source.close();}};source.onopen = function (evt) {console.log("开始订阅");}</script>
</div>
</body>
</html>

请注意上面的 html 实现,id 为 state 这个标签默认是不可见的;通过EventSource来实现 SSE(优点是实时且自带重试功能),并针对返回的结果进行了格式定义

  • 若接收到服务端 scan 消息,则修改 state 标签文案,并设置为可见
  • 若接收到服务端 login#cookie 格式数据,表示登录成功,#后面的为 cookie,设置本地 cookie,然后重定向到主页,并关闭长连接

其次在 script 标签中,如果需要访问传递的参数,请注意下面两点

  • 需要在 script 标签上添加th:inline="javascript"
  • [[${}]] 获取传递参数

3. sse 接口

前面登录的接口中,返回了一个sse的注册接口,客户端在访问登录页时,会访问这个接口,按照我们前面的 sse 教程文档,可以如下实现

private Map<String, SseEmitter> cache = new ConcurrentHashMap<>();@GetMapping(path = "subscribe", produces = {org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter subscribe(String id) {// 设置五分钟的超时时间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. 扫码接口

接下来就是扫描二维码进入授权页面的接口了,这个逻辑就比较简单了

@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) {// 告诉pc端,已经扫码了sseEmitter.send("scan");}// 授权同意的urlString url = "http://" + IpUtils.getLocalIP() + ":" + port + "/accept?id=" + id;model.addAttribute("url", url);return "scan";
}

用户扫码访问这个页面之后,会根据传过来的 id,定位对应的 pc 客户端,然后发送一个scan的信息

授权页面简单一点实现,加一个授权的超链就好,然后根据实际的情况补上用户 token(由于并没有独立的 app 和用户体系,所以下面作为演示,就随机生成一个 token 来替代)

<!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>扫码登录界面</title>
</head>
<body><div><div class="title">确定登录嘛?</div><div><a id="login">登录</a></div><script th:inline="javascript">// 生成uuid,模拟传递用户tokenfunction 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);});}// 获取实际的token,补齐参数,这里只是一个简单的模拟var url = [[${url}]];document.getElementById("login").href = url + "&token=" + guid();</script></div>
</body>
</html>

5. 授权接口

点击上面的授权超链之后,就表示登录成功了,我们后端的实现如下

@ResponseBody
@GetMapping(path = "accept")
public String accept(String id, String token) throws IOException {SseEmitter sseEmitter = cache.get(id);if (sseEmitter != null) {// 发送登录成功事件,并携带上用户的token,我们这里用cookie来保存tokensseEmitter.send("login#qrlogin=" + token);sseEmitter.complete();cache.remove(id);}return "登录成功: " + token;
}

6. 首页

用户授权成功之后,就会自动跳转到首页了,我们在首页就简单一点,搞一个欢迎的文案即可

@GetMapping(path = {"home", ""})
@ResponseBody
public String home(HttpServletRequest request) {Cookie[] cookies = request.getCookies();if (cookies == null || cookies.length == 0) {return "未登录!";}Optional<Cookie> cookie = Stream.of(cookies).filter(s -> s.getName().equalsIgnoreCase("qrlogin")).findFirst();return cookie.map(cookie1 -> "欢迎进入首页: " + cookie1.getValue()).orElse("未登录!");
}

7. 实测

到此一个完整的登录授权已经完成,可以进行实际操作演练了,下面是一个完整的演示截图(虽然我并没有真的用 app 进行扫描登录,而是识别二维码地址,在浏览器中进行授权,实际并不影响整个过程,你用二维扫一扫授权效果也是一样的)

6b270cbf0289652199e0df31be6cb2d6.png

请注意上面截图的几个关键点

  • 扫码之后,登录界面二维码下面会显示已扫描的文案
  • 授权成功之后,登录界面会主动跳转到首页,并显示欢迎 xxx,而且注意用户是一致的

8. 小结

实际的业务开发选择的方案可能和本文提出的并不太一样,也可能存在更优雅的实现方式(请有这方面经验的大佬布道一下),本文仅作为一个参考,不代表标准,不表示完全准确,如果把大家带入坑了,请留言(当然我是不会负责的 )

上面演示了徒手撸了一个二维码登录的示例工程,主要用到了一下技术点

  • qrcode-plugin:生成二维码,再次强烈安利一个私以为 java 生态下最好用二维码生成工具包 https://github.com/liuyueyi/quick-media/blob/master/plugins/qrcode-plugin[3] (虽然吹得比较凶,但我并没有收广告费,因为这也是我写的 )
  • SSE: 服务端推送事件,服务端单通道通信,实现消息推送
  • SpringBoot/Thymeleaf: 演示项目基础环境

最后,觉得不错的可以赞一下,加个好友有事没事聊一聊,关注个微信公众号支持一二,都是可以的嘛

III. 其他

0. 项目

相关博文

关于本篇博文,部分知识点可以查看以下几篇进行补全

  • 【SpringBoot WEB 系列】SSE 服务器发送事件详解
  • 【SpringBoot WEB 系列】异步请求知识点与使用姿势小结
  • 【SpringBoot WEB 系列】Thymeleaf 环境搭建

  • 工程:https://github.com/liuyueyi/spring-boot-demo[4]
  • 项目源码:https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-case/202-web-qrcode-login[5]

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰 Blog 个人博客 https://blog.hhui.top[6]
  • 一灰灰 Blog-Spring 专题博客 http://spring.hhui.top[7]

http://weixin.qq.com/r/FS9waAPEg178rUcL93oH (二维码自动识别)

参考资料

[1] https://github.com/liuyueyi/quick-media: https://github.com/liuyueyi/quick-media

[2] 聊一聊二维码扫描登录原理: https://juejin.im/post/5e83e716e51d4546c27bb559?utm_source=gold_browser_extension

[3] https://github.com/liuyueyi/quick-media/blob/master/plugins/qrcode-plugin: https://github.com/liuyueyi/quick-media/blob/master/plugins/qrcode-plugin

[4] https://github.com/liuyueyi/spring-boot-demo: https://github.com/liuyueyi/spring-boot-demo

[5] https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-case/202-web-qrcode-login: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-case/202-web-qrcode-login

[6] https://blog.hhui.top: https://blog.hhui.top

[7] http://spring.hhui.top: http://spring.hhui.top

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/571771.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenGL, GLSL, DirectX, HLSL中的矩阵存储形式

&#xff08;原文地址&#xff1a;http://alvincc-tech.blogspot.com/2010/10/opengl-glsl-directx-hlsl.html&#xff09; OpenGL, GLSL, DirectX, HLSL中的矩阵存储形式 OpenGL: 按列存储矩阵(column-major)。调用API形成的矩阵用来和一个列向量相乘&#xff0c;矩阵在左&am…

linux cpp标准库,标准库以及标准头文件

源文件通过编译可以生成目标文件(例如 GCC 下的 .o 和 Visual Studio 下的 .obj)&#xff0c;并提供一个头文件向外暴露接口&#xff0c;除了保护版权&#xff0c;还可以将散乱的文件打包&#xff0c;便于发布和使用。实际上我们一般不直接向用户提供目标文件&#xff0c;而是将…

inputstreamreader未关闭会导致oom_ThreadLocal 一定会导致内存泄露?

在面试的时候&#xff0c;ThreadLocal作为高并发常用工具经常会被问到。而面试官比较喜欢问的问题有以下两个&#xff1a;1、ThreadLocal是怎么实现来保证每个线程的变量副本的。2、ThreadLocal的内存泄露是怎么产生的&#xff0c;怎么避免内存泄露。首先我们来看第一个问题&am…

字符串的格式化

字符串作为一种常见的数据类型&#xff0c;也有其不同之处&#xff0c;其中最特别的当属字符串的格式化。 对于“格式化”估计很多的人有点懵&#xff0c;先来看一个例子。 >>> price of eggs: $%d % 3.5 字符串 price of eggs: $3 被格式化后的结果…

学生实验平台搭建c语言程序,c语言程序设计实验学生用.doc

c语言程序设计实验学生用C语言程序设计实验指导(学生用)计算机基础教研室《C语言程序设计》课程组2012年9月前 言《C语言程序设计》是计算机科学技术系面向全校理工科开设地一门专业平台课程.通过这门课程地学习,可以让学生了解程序设计地思想和方法,掌握高级语言程序设计地基本…

keras保存模型_TF2 8.模型保存与加载

举个例子&#xff1a;先训练出一个模型import 接下来第一种方法&#xff1a;只保留模型的参数&#xff1a;这个有2种方法&#xff1a;model.save_weights("adasd.h5")model.load_weights("adasd.h5") model.predict(x_test)model.save_weights(./checkpoin…

第一章 Burp Suite 安装和环境配置

Burp Suite是一个集成化的渗透测试工具&#xff0c;它集合了多种渗透测试组件&#xff0c;使我们自动化地或手工地能更好的完成对web应用的渗透测试和攻击。在渗透测试中&#xff0c;我们使用Burp Suite将使得测试工作变得更加容易和方便&#xff0c;即使在不需要娴熟的技巧的情…

mysql57服务无法启动_将mysqld.service服务加入到systemctl

在开始安装二进制MySQL的时候感觉都还挺好&#xff0c;就是在启动服务的时候比较麻烦&#xff0c;一开始是在Centos6下的感觉也没有什么费劲的;但是在Centos7下面还是有点不太适应&#xff0c;不过还好用用就熟悉了&#xff1b;说明一下&#xff0c;我的安装目录在/usr/local/m…

linux raid autodetect,软raid的建立

1 增加磁盘并分区(修改id)fdisk /dev/sdbCommand (m for help): pDisk /dev/sdb: 8589 MB, 8589934592 bytes255 heads, 63 sectors/track, 1044 cylindersUnits cylinders of 16065 * 512 8225280 bytesDevice Boot Start End Blocks Id System/dev/sd…

input readonly 光标显示问题

input readonly模式下在ie跟火狐访问的时候会有光标会出现&#xff0c;以下方法可解决这个问题 <input type"text" readonly unselectableon onfocus"this.blur()"> 1.unselectableon 是解决ie下光标出现的问题 2.οnfοcus"this.blur() 是解决…

c语言for循环的省略写法,C语言两种for循环写法分析

每个C程序员都知道同一个for循环语句可以有两种写法:A: for (i 0; i B: for (i cnt; i > 0; i--){ }前几天,DEBUG的时候, 发现采用A写法的代码反汇编出来有BUG.当时没有时间记录,环境也没有保存下来.今天尝试重现,又没来出现上次的问题...很奇怪.很久很久以前也听说过这两…

python文字游戏 生成数字菜单_pygame游戏之旅 游戏中添加显示文字

本文为大家分享了pygame游戏之旅的第5篇&#xff0c;供大家参考&#xff0c;具体内容如下 在游戏中添加显示文字&#xff1a; 这里自己定义一个crash函数接口&#xff1a; def crash(): message_diaplay(You Crashed) 然后实现接口函数message_display(text) def message_diapl…

快速排序的改进

package com.txq.test; /*** quicksort,三方面改进&#xff1a;①三数中值选择枢纽元②容量小的时候使用插入排序③重复元素的处理* author XueQiang Tong* date 2017/10/25*/ public class QS {public void quicksort(int []arr,int low,int high){int first low;int last h…

23根火柴游戏 c语言,23 根火柴游戏

#includegt;int main(){int g 23;int k 3;int b, c;printf("这里是23 根火柴游戏&#xff01;&#xff01;\n");printf("注意&#xff1a;最大移动火柴数目为三根\n");do{printf("请输入移动的火柴数目&#xff1a;\n");scanf("%d",…

springboot netty给特定客户端推送_Spring Boot 又升级了?2.0 你搞懂了吗?!

【小宅按】作为知名互联网公司都在用的技术&#xff0c;Spring Boot 2.0 的更新引起了很大的关注&#xff0c;本文将分为三部分解读 2.0 的更新&#xff1a;第一类&#xff0c;基础环境升级&#xff1b;第二类&#xff0c;默认软件替换和优化&#xff1b;第三类&#xff0c;新技…

OSI七层模型与TCP/IP五层模型详解

博主是搞是个FPGA的&#xff0c;一直没有真正的研究过以太网相关的技术&#xff0c;现在终于能静下心学习一下&#xff0c;希望自己能更深入的掌握这项最基本的通信接口技术。下面就开始搞了。 一、OSI参考模型 今天我们先学习一下以太网最基本也是重要的知识——OSI参考模型。…

c是过程化语言吗数据库,关于SQL错误的是()A、所有数据库的公共语言B、非过程化的C、统一的语言D、所有用SQL缩写的程序都...

关于SQL错误的是()A、所有数据库的公共语言B、非过程化的C、统一的语言D、所有用SQL缩写的程序都更多相关问题[多选] 在彩色电视机遥控系统中&#xff0c;属于模拟量控制的有()等几种。[多选] 在色度信号记录处理中&#xff0c;家用录像机一般都要对色度信号经过()等处理。[多选…

python建立数据库和基本表_python基础 — 链接 Mysql 创建 数据库和创表

重点&#xff1a; &#xff11;. 链接服务器的数据库 &#xff12;. 创建表和格式 &#xff13;. 插入多行数据 import pymysql try: hostxxx userxxx passwdxxx dbtest01 port3306 Table_namekaka5 # 链接到服务器 db pymysql.connect(host, user, passwd, db, port) # 创…

c语言陷阱试题,超级经典计算机二级C语言陷阱考试题.doc

超级经典计算机二级C语言陷阱考试题超级经典计算机二级C语言陷阱考试题若有定义&#xff1a;int a[2][3],则对a数组的第i行j列元素地址的正确引用为___d___.a)*(a[i]j) b)(ai) c)*(aj) d)a[i]j以下正确的程序段是_________.a)char str[20]; b)char *p;scanf("%s",&am…

python开发跟淘宝有关联微_Python_淘宝用户行为分析

一、数据导入与清洗 源数据量有1亿余条&#xff0c;为减轻计算量&#xff0c;抽样总量的20%用于计算分析 #codinggbk import numpy as py import pandas as pd import datetime import os os.chdir(D:/pythonlily/test1) datapd.read_csv(UserBehavior.csv,headerNone) data.co…