Java SpringBoot实现简易扫码登录流程 附项目代码

news/2025/2/27 16:16:21/文章来源:https://www.cnblogs.com/anyuan/p/18738593

先总结流程:

  1. 移动端请求扫码登录,服务端生成二维码并缓存二维码ID和状态,将二维码的Base64格式返回给前端展示;
  2. PC端页面轮询检查二维码状态;
  3. 手机扫码后调用扫码接口,携带移动端的Token和二维码ID请求服务端,服务端根据扫码的信息生成临时Totken,将二维码状态更新为已扫码;
  4. PC端页面轮询检查二维码状态;
  5. 用户在移动端确认登录后,携带临时Token和二维码ID请求服务端,服务端校验临时Token,将二维码状态更新为已确认,并生成效期Token;
  6. 电脑端轮询到二维码状态为已确认后,获取效期Token登录成功;


先看简易流程图, 流程图planUML文件在文末的 Github 链接:


流程大概明白后,再看项目简易效果。

启动项目,进入登录界面:http://localhost:8080/login,控制台查看Network, generate 接口获取到了二维码和uuid等信息,同时页面以 uuid 轮询获取二维码最新状态

先使用postMan模拟扫码动作,请求:http://localhost:8080/api/qrcode/scan/2aa6dd82-96e9-4c13-a476-1fa07b913ae

再模拟确认登录,请求:localhost:8080/api/qrcode/confirm/2aa6dd82-96e9-4c13-a476-1fa07b913ae4

如果用户一直没扫码或扫码后一直没确认,二维码会自动提示过期:

再上代码:先创建一个项目,pom.xml 文件引入二维码以及Jwt扩展包:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.4.1</version></dependency><dependency><groupId>com.google.zxing</groupId><artifactId>javase</artifactId><version>3.4.1</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId><version>2.1.6.RELEASE</version></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.28</version></dependency>
</dependencies>
pom.xml

 登录页面controller:HomeController

package com.cnblog.qrcodeLogIn.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;/*** @author AnYuan*/@Controller
@Slf4j
public class HomeController {@GetMapping("/login")public ModelAndView login() {log.info("用户进入登录页面");return new ModelAndView("login");}@GetMapping("/home")public ModelAndView home() {log.info("用户扫码登录成功");return new ModelAndView("home");}}

获取二维码生成、更新的controller: LoginController。为什么使用两个controller,是因为 HomeContrller 使用的是模板页面,使用的是 @Controller 注解声明的,而二维码功能接口是使用 @RestControlle注解声明的

package com.cnblog.qrcodeLogIn.controller;import com.alibaba.fastjson2.JSONObject;
import com.cnblog.qrcodeLogIn.dto.LoginInfoDTO;
import com.cnblog.qrcodeLogIn.vo.ResponseVO;
import com.cnblog.qrcodeLogIn.enums.LoginStatusEnum;
import com.cnblog.qrcodeLogIn.utils.JwtUtil;
import com.cnblog.qrcodeLogIn.utils.QRCodeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;import java.util.UUID;
import java.util.concurrent.TimeUnit;/*** @author AnYuan*/@Slf4j
@RestController
@RequestMapping("/api/qrcode")
public class LoginController {@Autowiredprivate StringRedisTemplate stringRedisTemplate;private String cacheKey(String uuid) {return "users:login:" + uuid;}private void cache(LoginInfoDTO loginInfoDTO) {// 获取登录缓存信息,有效期2分钟stringRedisTemplate.opsForValue().set(cacheKey(loginInfoDTO.getUuid()), JSONObject.toJSONString(loginInfoDTO), 2, TimeUnit.MINUTES);}private LoginInfoDTO getCache(String uuid) {// 获取登录缓存信息String s = stringRedisTemplate.opsForValue().get(cacheKey(uuid));return s == null ? null : JSONObject.parseObject(s, LoginInfoDTO.class);}/*** 生成二维码* @return ResponseEntity* @throws Exception*/@GetMapping("/generate")public ResponseEntity<ResponseVO> generateQRCode() throws Exception {String uuid = UUID.randomUUID().toString();String base64QR = QRCodeUtil.generateQRCode(uuid, 200, 200);LoginInfoDTO loginInfoDTO = new LoginInfoDTO();loginInfoDTO.setStatus(LoginStatusEnum.UNSCANNED.name());loginInfoDTO.setUuid(uuid);// 二维码uuid绑定,存入缓存
        cache(loginInfoDTO);// 返回生成的二维码信息ResponseVO vo = ResponseVO.builder().uuid(uuid).qrcode("data:image/png;base64," + base64QR).build();log.info("-------生成二维码成功:{}-------", uuid);return ResponseEntity.ok(vo);}/*** 检查扫码状态* @param uuid* @return*/@GetMapping("/check/{uuid}")public ResponseEntity<?> checkStatus(@PathVariable String uuid) {LoginInfoDTO loginInfoDTO = getCache(uuid);if (loginInfoDTO == null) {return ResponseEntity.status(410).body("二维码已过期");}String token = "";if (LoginStatusEnum.CONFIRMED.name().equals(loginInfoDTO.getStatus())) {token = JwtUtil.generateAuthToken(uuid);}ResponseVO vo = ResponseVO.builder().token(token).status(loginInfoDTO.getStatus()).build();log.info("-------校验二维码状态uuid:{}, 状态:{}-------", uuid, loginInfoDTO.getStatus());return ResponseEntity.ok(vo);}/*** 手机端确认登录* @param uuid* @return*/@PostMapping("/scan/{uuid}")public ResponseEntity<?> scanQrCode(@PathVariable String uuid) {LoginInfoDTO loginInfoDTO = getCache(uuid);loginInfoDTO.setStatus(LoginStatusEnum.SCANNED.name());cache(loginInfoDTO);log.info("-------扫码成功uuid:{}-------", uuid);return ResponseEntity.ok().build();}/*** 手机端确认登录* @param uuid* @return*/@PostMapping("/confirm/{uuid}")public ResponseEntity<?> confirm(@PathVariable String uuid) {LoginInfoDTO loginInfoDTO = getCache(uuid);loginInfoDTO.setStatus(LoginStatusEnum.CONFIRMED.name());cache(loginInfoDTO);log.info("-------确认登录成功uuid:{}-------", uuid);return ResponseEntity.ok().build();}
}

二维码状态枚举类:LoginStatusEnum

package com.cnblog.qrcodeLogIn.enums;import lombok.Getter;@Getter
public enum LoginStatusEnum {UNSCANNED("未扫描"),SCANNED("已扫描"),CONFIRMED("已确认");private String desc;LoginStatusEnum(String desc) {this.desc = desc;}
}

记录二维码状态存储到Redis的实体DTO:LoginInfoDTO

package com.cnblog.qrcodeLogIn.dto;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfoDTO {/*** 唯一标识*/private String uuid;/*** 设备号*/private String device;/*** jwt令牌*/private String token;/*** 扫码状态*/private String status;
}

返回给前端页面是对象VO:ResponseVO

package com.cnblog.qrcodeLogIn.vo;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResponseVO {/*** 唯一标识*/private String uuid;/*** 登录二维码*/private String qrcode;/*** jwt令牌*/private String token;/*** 扫码状态*/private String status;
}

生成Jwt令牌的工具类:

package com.cnblog.qrcodeLogIn.utils;import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;import java.nio.charset.StandardCharsets;
import java.util.Date;public class JwtUtil {private static final String SECRET_KEY = "9dad5e7e-bcb7-438f-b39e-ad8c67814915";public static  String generateAuthToken(String uuid) {// 生成JWT或其他形式令牌return Jwts.builder().setSubject(uuid).setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时过期
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes(StandardCharsets.UTF_8)).compact();}
}

生成二维码的工具类:

package com.cnblog.qrcodeLogIn.utils;import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Hashtable;public class QRCodeUtil {public static String generateQRCode(String content, int width, int height)throws WriterException, IOException {Hashtable<EncodeHintType, Object> hints = new Hashtable<>();hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");BitMatrix matrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);ByteArrayOutputStream outputStream = new ByteArrayOutputStream();MatrixToImageWriter.writeToStream(matrix, "PNG", outputStream);
return Base64.getEncoder().encodeToString(outputStream.toByteArray()); } }

一个登录页面:login.html

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>登录界面</title><style>body {font-family: Arial, sans-serif;background-color: #f4f4f4;display: flex;justify-content: center;align-items: center;height: 100vh;margin: 0;}.login-container {background-color: #fff;padding: 40px;border-radius: 10px;box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);width: 100%;max-width: 400px;text-align: center;}.login-container h2 {margin-bottom: 20px;color: #333;}.login-container .forgot-password {margin-top: 15px;display: block;color: #007bff;text-decoration: none;font-size: 14px;}.login-container .forgot-password:hover {text-decoration: underline;}.login-container .register-link {margin-top: 20px;font-size: 14px;color: #666;}.login-container .register-link a {color: #007bff;text-decoration: none;}.login-container .register-link a:hover {text-decoration: underline;}.blur-image {filter: blur(3px);}</style>
</head>
<body><div class="login-container"><h2>扫码登录</h2><div id="qrcode-container"><img id="qrcode-img" src="" alt="QR Code" class=""><div id="status"></div></div><a href="#" class="forgot-password" id="forgetPsw">忘记密码?</a><div class="register-link" id="signUp">还没有账号? <a href="#">立即注册</a></div>
</div>
<script>let currentUUID = null;let host = "http://localhost:8080";// 初始化二维码function initQRCode() {fetch(host+'/api/qrcode/generate').then(res => res.json()).then(data => {currentUUID = data.uuid;document.getElementById('qrcode-img').src = data.qrcode;startPolling();});}// 轮询检查状态function startPolling() {const interval = setInterval(() => {fetch(host+`/api/qrcode/check/${currentUUID}`).then(res => {if(res.status === 410) {clearInterval(interval);showStatus('二维码已过期,请刷新页面');return;}return res.json();}).then(data => {if(data.status === 'CONFIRMED') {clearInterval(interval);handleLoginSuccess(data.token);} else if(data.status === 'SCANNED') {showStatus('已扫描,请在手机上确认登录');}});}, 2000);}function handleLoginSuccess(token) {localStorage.setItem('authToken', token);window.location.href = '/home';}function showStatus(msg) {document.getElementById('status').innerHTML = msg;document.getElementById('forgetPsw').innerHTML="";document.getElementById('signUp').innerHTML="";document.getElementById('qrcode-img').className = 'blur-image';}// 初始化
    initQRCode();
</script>
</body>
</html>
login.html

最后是扫码登录成功的跳转首页:home.html

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>登录成功-首页</title><style>body {font-family: Arial, sans-serif;background-color: #f4f4f4;display: flex;justify-content: center;align-items: center;height: 100vh;margin: 0;}.login-container {background-color: #fff;padding: 40px;border-radius: 10px;box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);width: 100%;max-width: 400px;text-align: center;}.login-container h2 {margin-bottom: 20px;color: #333;}</style>
</head>
<body>
<div class="login-container"><h2>扫码登录成功</h2>
</div>
</body>
</html>
home.html

流程中生成临时token的操作没有细写,可根据流程图在对应的逻辑里添加即可。

完整项目结构:

src/main
└── java└── com└── cnblog└── qrcodeLogIn├── controller       // 控制器├── dto         // 实体类├── enums      // 枚举类├── utils         // 工具类├── vo         // 响应类└── QrcodeLogInApplication.java
└── resources└── templates     // html模板文件└── home.html└── login.html

 

本篇代码Github:https://github.com/Journeyerr/cnblogs/tree/master/qrcodeLogIn

 

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

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

相关文章

Salesforce宣布2025年不招工程师,技术岗位真的会消失吗?

Salesforce CEO Marc Benioff 最近放出了一条让科技圈热议的消息:2025年,Salesforce将不再招聘新的软件工程师。作为全球最大的CRM软件提供商之一,这一决定引发了许多人对AI对就业市场影响的思考。 AI改变工作模式,工程师岗位需求减少 Marc Benioff 在采访中透露,2024年,…

mysql查询语句

一、查询语句 1、select查询一个表中的所有数据 格式: select * from 表名 ; 案例: select * from student ;2、查询部分字段信息数据 格式: select 字段1,字段2 from 表名; 案例: select name,math from student;3、查询字段可以用as 取别名 格式: select 字…

Vue3安装配置+VSCode开发环境搭建,超详细保姆级教程(图文)

目录1. node.js 下载安装2. 查看 node.js 是否安装成功3. 配置 npm 下载的默认安装、缓存环境(1) 添加文件夹(2) 设置系统环境变量(3) 修改下载模块的存放路径4. npm 镜像源配置(1) 输入cmd打开命令提示符窗口,(2) 如果配置报错(3) 查看镜像配置是否成功5. 安装 vue-cli 工具6.…

Windows下Redis哨兵模式配置以及在.NetCore中使用StackExchange.Redis连接哨兵

一,Redis哨兵模式配置 1,下载Redis,然后解压复制5个文件夹分别如下命名。 2,哨兵模式配置 (1)修改主节点Redis-6379中redis.windows.conf配置文件如下 (2)修改从节点Redis-6380中redis.windows.conf配置文件如下(3)配置哨兵,在哨兵文件夹下添加Sentinel.conf配置文件…

zabbix监控http

一、模版复制与配置变更 1、模版复制2、修改宏 全克隆并修改好宏端口二、测试httpd服务状态 1、检查模块是否启用 apachectl -M | grep status修复 #ServerName www.example.com:80 ServerName 192.168.0.152:13000 重新执行后正常2、检查状态 http://112.81.86.33:13001/serve…

Ubuntu 22.04 或更高版本的系统中安装.NET Core 3.1的解决方案(使用1panel进行可视化演示)

第一步:先安装长期支持版本.NET 6 或更高 (这一步不是必须的,如果是干净的服务器建议这么做,因为安装完后会自动创建路径和环境变量方便后续操作)安装方法sudo apt update sudo apt install -y dotnet6如果安装时出现异常:A fatal error occurred. The folder [/usr/lib/dot…

考古新视野:LiDAR 揭开雨林下的玛雅古城!

一、当科技遇见文明:LiDAR 的考古革命茂密的雨林曾是考古学家的噩梦——藤蔓缠绕的树冠遮蔽了地面,人力勘探耗时费力,无数古代遗迹深藏其中。然而,激光雷达(LiDAR)技术的出现,如同一把“数字X光刀”,穿透了雨林的绿色屏障,将玛雅文明的失落之城从历史的尘埃中重新点亮…

mysql知识面试day2

mysql具有哪些锁 按锁的粒度分配:行级锁,表级锁,页级锁。mvcc的实现原理 MVCC--一份数据保留多个版本的一种方式,查询时通过readview和版本链获得对应版本的数据 好处:提升并发性能,对于高并发场景,mvcc比行级锁开销更小 实现原理 MVCC的实现依赖于版本链,版本链具有三个隐…

AI 搜索你的所有笔记!思源笔记 +Cursor+MCP Server——打造你的个人专属 AI 资料库!(AI 大模型搜索笔记、内容总结、大纲凝练、RAG 搜索)

AI 搜索你的所有笔记!思源笔记 +Cursor+MCP Server ——打造你的个人专属 AI 资料库!(AI 大模型搜索笔记、内容总结、大纲凝练、RAG 搜索) 前排提示:本文撰写于 2025 年 2 月,仅仅离 Anthropic 发布的 MCP 协议过去不到 3 个月,因此本文很多接入 MCP 的方式还略显复杂。…

LLM大模型:deepseek浅度解析(四):Native Sparse Attention NSA原理

deepseek又整活了啊,2025.2.16的时候又发布了 "Native Sparse Attention: Hardware-Aligned and Natively Trainable Sparse Attention",核心是解决attention计算耗时耗算力的问题!NSA具体又是怎么做的了?回忆一下:attention效果好的核心原因,就是Q*K得到了tok…

聊一聊:Air8000能解决哪些社会问题?

Air8000能解决什么社会问题呢?当前我们认为可以解决如下的问题: 问题一 硬件:成本高,备货压力大,稳定性差 嵌入式的一些常用的功能,比如GPIO、4G、Wi-Fi、蓝牙、定位、充电、升压、处理器等等,是项目上常用的功能。 如果每个都是模块,组合起来成本不菲。 Air8000的定价…

Open开发:CSDK与LuatOS的深度剖析

究竟要不要支持CSDK开发? 我们先来了解一下4G模组的软件架构。目前,4G模组内部的软件架构无一例外都是用C语言开发的,仅在底层使用了少量汇编语言。 从技术角度看,让用户使用C语言开发应用似乎顺理成章。毕竟C语言功能强大,运行效率极高。 然而,C语言在物联网行业的应用存…