前言:
本文基于SpringBoot框架实现了一个完整的扫码登录系统。
扫码登录流程:
- Web端向服务器请求生成唯一二维码
- 服务器生成二维码图片并返回
- 用户通过手机App扫描该二维码
- 手机App发送确认请求到服务器
- 服务器通知Web端登录成功
- Web端完成登录流程
后端实现
Maven依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>qrcode-login</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>qrcode-login</name>
<description>SpringBoot QR Code Login Demo</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- ZXing for QR Code generation -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.1</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
配置文件
在application.yaml中添加配置:
spring:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 5000ms
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
# 二维码配置
qrcode:
# 二维码有效期(秒)
expire:
seconds: 300
width: 100
height: 100代码:
package com.example.qrcodelogin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class QrcodeLoginApplication {
public static void main(String[] args) {
SpringApplication.run(QrcodeLoginApplication.class, args);
}
}
Redis配置
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 配置ObjectMapper,添加类型信息
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
// 使用Jackson2JsonRedisSerializer作为值的序列化器
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置key和value的序列化方式
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
websocket配置
package com.example.qrcodelogin.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(qrCodeWebSocketHandler(), "/ws/qrcode")
.setAllowedOrigins("*");
}
@Bean
public QrCodeWebSocketHandler qrCodeWebSocketHandler() {
return new QrCodeWebSocketHandler();
}
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocket处理器
package com.example.qrcodelogin.config;
import com.example.qrcodelogin.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class QrCodeWebSocketHandler extends TextWebSocketHandler {
// 存储所有WebSocket连接,key为二维码ID
private static final Map<String, WebSocketSession> SESSIONS = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
log.info("WebSocket connection established: {}", session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("Received message: {}", payload);
Map<String, String> msgMap = JsonUtil.fromJson(payload, Map.class);
if (msgMap != null && msgMap.containsKey("qrCodeId")) {
String qrCodeId = msgMap.get("qrCodeId");
log.info("Client subscribed to QR code: {}", qrCodeId);
// 将会话与二维码ID关联
SESSIONS.put(qrCodeId, session);
// 发送确认消息
session.sendMessage(new TextMessage("{"type":"CONNECTED","message":"Connected to QR code: " + qrCodeId + ""}"));
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
log.info("WebSocket connection closed: {}, status: {}", session.getId(), status);
// 移除会话
SESSIONS.entrySet().removeIf(entry -> entry.getValue().getId().equals(session.getId()));
}
// 向指定二维码ID的客户端发送消息
public void sendMessage(String qrCodeId, Object message) {
WebSocketSession session = SESSIONS.get(qrCodeId);
if (session != null && session.isOpen()) {
try {
session.sendMessage(new TextMessage(JsonUtil.toJson(message)));
} catch (IOException e) {
log.error("Failed to send message to WebSocket client", e);
}
}
}
}
QRCodeStatus.java - 二维码状态类
package com.example.qrcodelogin.model;
import lombok.Data;
@Data
public class QRCodeStatus {
public static final String WAITING = "WAITING"; // 等待扫描
public static final String SCANNED = "SCANNED"; // 已扫描
public static final String CONFIRMED = "CONFIRMED"; // 已确认
public static final String CANCELLED = "CANCELLED"; // 已取消
public static final String EXPIRED = "EXPIRED"; // 已过期
private String qrCodeId; // 二维码ID
private String status; // 状态
private UserInfo userInfo; // 用户信息
private long createTime; // 创建时间
public QRCodeStatus() {
this.createTime = System.currentTimeMillis();
}
public QRCodeStatus(String qrCodeId, String status) {
this.qrCodeId = qrCodeId;
this.status = status;
this.createTime = System.currentTimeMillis();
}
}UserInfo.java - 用户信息类
package com.example.qrcodelogin.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
private String userId;
private String username;
private String avatar;
private String email;
private String token;
}JsonUtil.java - JSON工具类
package com.example.qrcodelogin.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class JsonUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static String toJson(Object object) {
try {
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
log.error("Convert object to json failed", e);
return null;
}
}
public static <T> T fromJson(String json, Class<T> clazz) {
try {
return objectMapper.readValue(json, clazz);
} catch (JsonProcessingException e) {
log.error("Convert json to object failed", e);
return null;
}
}
}QR码生成工具类
package com.example.qrcodelogin.util;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class QRCodeUtil {
/**
* 生成二维码图片的字节数组
*/
public static byte[] generateQRCodeImage(String text, int width, int height) throws WriterException, IOException {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hints.put(EncodeHintType.MARGIN, 2);
BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hints);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream);
return outputStream.toByteArray();
}
}服务类
package com.example.qrcodelogin.service;
import com.example.qrcodelogin.config.QrCodeWebSocketHandler;
import com.example.qrcodelogin.model.QRCodeStatus;
import com.example.qrcodelogin.model.UserInfo;
import com.example.qrcodelogin.util.QRCodeUtil;
import com.google.zxing.WriterException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class QRCodeService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private QrCodeWebSocketHandler webSocketHandler;
@Value("${qrcode.expire.seconds}")
private long qrCodeExpireSeconds;
@Value("${qrcode.width}")
private int qrCodeWidth;
@Value("${qrcode.height}")
private int qrCodeHeight;
private static final String QR_CODE_PREFIX = "qrcode:";
/**
* 生成二维码
*/
public QRCodeStatus generateQRCode() {
String qrCodeId = UUID.randomUUID().toString();
QRCodeStatus qrCodeStatus = new QRCodeStatus(qrCodeId, QRCodeStatus.WAITING);
// 存储到Redis并设置过期时间
redisTemplate.opsForValue().set(QR_CODE_PREFIX + qrCodeId, qrCodeStatus, qrCodeExpireSeconds, TimeUnit.SECONDS);
return qrCodeStatus;
}
/**
* 生成二维码图片
*/
public byte[] generateQRCodeImage(String qrCodeId, String baseUrl) {
try {
// 构建二维码内容
String qrCodeContent = baseUrl + "/mobile.html?qrCodeId=" + qrCodeId;
// 生成二维码图片
return QRCodeUtil.generateQRCodeImage(qrCodeContent, qrCodeWidth, qrCodeHeight);
} catch (WriterException | IOException e) {
log.error("Failed to generate QR code image", e);
return null;
}
}
/**
* 获取二维码状态
*/
public QRCodeStatus getQRCodeStatus(String qrCodeId) {
Object obj = redisTemplate.opsForValue().get(QR_CODE_PREFIX + qrCodeId);
if (obj instanceof QRCodeStatus) {
return (QRCodeStatus) obj;
}
return null;
}
/**
* 更新二维码状态
*/
public boolean updateQRCodeStatus(String qrCodeId, String status) {
QRCodeStatus qrCodeStatus = getQRCodeStatus(qrCodeId);
if (qrCodeStatus == null) {
return false;
}
qrCodeStatus.setStatus(status);
redisTemplate.opsForValue().set(QR_CODE_PREFIX + qrCodeId, qrCodeStatus, qrCodeExpireSeconds, TimeUnit.SECONDS);
// 通过WebSocket发送状态更新
Map<String, Object> message = new HashMap<>();
message.put("type", "STATUS_CHANGE");
message.put("status", status);
webSocketHandler.sendMessage(qrCodeId, message);
return true;
}
/**
* 确认登录
*/
public boolean confirmLogin(String qrCodeId, UserInfo userInfo) {
QRCodeStatus qrCodeStatus = getQRCodeStatus(qrCodeId);
if (qrCodeStatus == null || !QRCodeStatus.SCANNED.equals(qrCodeStatus.getStatus())) {
return false;
}
qrCodeStatus.setStatus(QRCodeStatus.CONFIRMED);
qrCodeStatus.setUserInfo(userInfo);
redisTemplate.opsForValue().set(QR_CODE_PREFIX + qrCodeId, qrCodeStatus, qrCodeExpireSeconds, TimeUnit.SECONDS);
// 通过WebSocket发送状态更新
Map<String, Object> message = new HashMap<>();
message.put("type", "STATUS_CHANGE");
message.put("status", QRCodeStatus.CONFIRMED);
message.put("userInfo", userInfo);
webSocketHandler.sendMessage(qrCodeId, message);
return true;
}
/**
* 取消登录
*/
public boolean cancelLogin(String qrCodeId) {
QRCodeStatus qrCodeStatus = getQRCodeStatus(qrCodeId);
if (qrCodeStatus == null) {
return false;
}
qrCodeStatus.setStatus(QRCodeStatus.CANCELLED);
redisTemplate.opsForValue().set(QR_CODE_PREFIX + qrCodeId, qrCodeStatus, qrCodeExpireSeconds, TimeUnit.SECONDS);
// 通过WebSocket发送状态更新
Map<String, Object> message = new HashMap<>();
message.put("type", "STATUS_CHANGE");
message.put("status", QRCodeStatus.CANCELLED);
webSocketHandler.sendMessage(qrCodeId, message);
return true;
}
/**
* 定时检查并清理过期的二维码
*/
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void cleanExpiredQRCodes() {
long currentTime = System.currentTimeMillis();
long expireTime = currentTime - qrCodeExpireSeconds * 1000;
// 查找所有二维码记录
Set<String> keys = redisTemplate.keys(QR_CODE_PREFIX + "*");
if (keys == null || keys.isEmpty()) {
return;
}
for (String key : keys) {
Object obj = redisTemplate.opsForValue().get(key);
if (obj instanceof QRCodeStatus) {
QRCodeStatus status = (QRCodeStatus) obj;
// 检查创建时间是否超过过期时间
if (status.getCreateTime() < expireTime && !QRCodeStatus.EXPIRED.equals(status.getStatus())) {
status.setStatus(QRCodeStatus.EXPIRED);
redisTemplate.opsForValue().set(key, status, 60, TimeUnit.SECONDS); // 设置一个短的过期时间,让客户端有机会收到过期通知
// 发送过期通知
Map<String, Object> message = new HashMap<>();
message.put("type", "STATUS_CHANGE");
message.put("status", QRCodeStatus.EXPIRED);
webSocketHandler.sendMessage(status.getQrCodeId(), message);
log.info("QR code expired: {}", status.getQrCodeId());
}
}
}
}
}
UserService.java - 用户服务类
package com.example.qrcodelogin.service;
import com.example.qrcodelogin.model.UserInfo;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Service
public class UserService {
// 模拟用户数据库
private static final Map<String, UserInfo> USER_DB = new HashMap<>();
static {
// 添加一些测试用户
USER_DB.put("user1", new UserInfo(
"user1",
"张三",
"https://api.dicebear.com/7.x/avataaars/svg?seed=user1",
"zhangsan@example.com",
null
));
USER_DB.put("user2", new UserInfo(
"user2",
"李四",
"https://api.dicebear.com/7.x/avataaars/svg?seed=user2",
"lisi@example.com",
null
));
}
/**
* 获取所有用户
*/
public Map<String, UserInfo> getAllUsers() {
return USER_DB;
}
/**
* 模拟登录
*/
public UserInfo login(String userId) {
UserInfo userInfo = USER_DB.get(userId);
if (userInfo != null) {
// 生成一个新的token
String token = UUID.randomUUID().toString();
userInfo.setToken(token);
return userInfo;
}
return null;
}
/**
* 验证token
*/
public UserInfo validateToken(String token) {
// 简单模拟,实际应用中应该有更复杂的token验证逻辑
for (UserInfo user : USER_DB.values()) {
if (token != null && token.equals(user.getToken())) {
return user;
}
}
return null;
}
}QRCodeController.java - 二维码相关API
package com.example.qrcodelogin.controller;
import com.example.qrcodelogin.model.QRCodeStatus;
import com.example.qrcodelogin.model.UserInfo;
import com.example.qrcodelogin.service.QRCodeService;
import com.example.qrcodelogin.service.UserService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/qrcode")
public class QRCodeController {
@Autowired
private QRCodeService qrCodeService;
@Autowired
private UserService userService;
/**
* 生成二维码
*/
@GetMapping("/generate")
public ResponseEntity<QRCodeStatus> generateQRCode() {
QRCodeStatus qrCodeStatus = qrCodeService.generateQRCode();
log.info("Generated QR code: {}", qrCodeStatus.getQrCodeId());
return ResponseEntity.ok(qrCodeStatus);
}
/**
* 获取二维码图片
*/
@GetMapping(value = "/image/{qrCodeId}", produces = MediaType.IMAGE_PNG_VALUE)
public ResponseEntity<byte[]> getQRCodeImage(@PathVariable String qrCodeId, HttpServletRequest request) {
// 获取基础URL
String baseUrl = request.getScheme() + "://" + request.getServerName();
if (request.getServerPort() != 80 && request.getServerPort() != 443) {
baseUrl += ":" + request.getServerPort();
}
byte[] qrCodeImage = qrCodeService.generateQRCodeImage(qrCodeId, baseUrl);
if (qrCodeImage != null) {
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.body(qrCodeImage);
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
/**
* 扫描二维码
*/
@PostMapping("/scan")
public ResponseEntity<String> scanQRCode(@RequestBody Map<String, String> request) {
String qrCodeId = request.get("qrCodeId");
if (qrCodeId == null) {
return ResponseEntity.badRequest().body("QR code ID is required");
}
boolean updated = qrCodeService.updateQRCodeStatus(qrCodeId, QRCodeStatus.SCANNED);
if (!updated) {
return ResponseEntity.badRequest().body("Invalid QR code");
}
log.info("QR code scanned: {}", qrCodeId);
return ResponseEntity.ok("Scanned successfully");
}
/**
* 确认登录
*/
@PostMapping("/confirm")
public ResponseEntity<String> confirmLogin(@RequestBody ConfirmLoginRequest request) {
if (request.getQrCodeId() == null || request.getUserId() == null) {
return ResponseEntity.badRequest().body("QR code ID and user ID are required");
}
// 模拟用户登录
UserInfo userInfo = userService.login(request.getUserId());
if (userInfo == null) {
return ResponseEntity.badRequest().body("User not found");
}
boolean confirmed = qrCodeService.confirmLogin(request.getQrCodeId(), userInfo);
if (!confirmed) {
return ResponseEntity.badRequest().body("Invalid QR code or status");
}
log.info("Login confirmed: {}, user: {}", request.getQrCodeId(), request.getUserId());
return ResponseEntity.ok("Login confirmed successfully");
}
/**
* 取消登录
*/
@PostMapping("/cancel")
public ResponseEntity<String> cancelLogin(@RequestBody Map<String, String> request) {
String qrCodeId = request.get("qrCodeId");
if (qrCodeId == null) {
return ResponseEntity.badRequest().body("QR code ID is required");
}
boolean cancelled = qrCodeService.cancelLogin(qrCodeId);
if (!cancelled) {
return ResponseEntity.badRequest().body("Invalid QR code");
}
log.info("Login cancelled: {}", qrCodeId);
return ResponseEntity.ok("Login cancelled successfully");
}
/**
* 获取二维码状态
*/
@GetMapping("/status/{qrCodeId}")
public ResponseEntity<QRCodeStatus> getQRCodeStatus(@PathVariable String qrCodeId) {
QRCodeStatus qrCodeStatus = qrCodeService.getQRCodeStatus(qrCodeId);
if (qrCodeStatus == null) {
return ResponseEntity.badRequest().body(null);
}
return ResponseEntity.ok(qrCodeStatus);
}
@Data
public static class ConfirmLoginRequest {
private String qrCodeId;
private String userId;
}
}LoginController.java - 登录相关API
package com.example.qrcodelogin.controller;
import com.example.qrcodelogin.model.UserInfo;
import com.example.qrcodelogin.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/auth")
public class LoginController {
@Autowired
private UserService userService;
/**
* 验证token并获取用户信息
*/
@PostMapping("/validate")
public ResponseEntity<UserInfo> validateToken(@RequestBody Map<String, String> request) {
String token = request.get("token");
if (token == null) {
return ResponseEntity.badRequest().body(null);
}
UserInfo userInfo = userService.validateToken(token);
if (userInfo == null) {
return ResponseEntity.badRequest().body(null);
}
log.info("Token validated for user: {}", userInfo.getUsername());
return ResponseEntity.ok(userInfo);
}
/**
* 获取可用的测试用户列表 (仅用于演示)
*/
@GetMapping("/users")
public ResponseEntity<Map<String, UserInfo>> getTestUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
}总结
上述是后台实现springboot扫码实现登录的全业务流程代码。通过调用接口或前端页面触发登录操作流程,后台执行登录请求,实现扫码登录功能。
