SpringBoot扫码登录实现

B站影视 内地电影 2025-08-31 12:55 5

摘要:在移动互联网时代,扫码登录已成为Web应用不可或缺的登录方式。本文基于SpringBoot框架实现了一个完整的

在移动互联网时代,扫码登录已成为 Web 应用不可或缺的登录方式。

本文基于 SpringBoot 框架实现了一个完整的扫码登录系统 DEMO。

Web端向服务器请求生成唯一二维码服务器生成二维码图片并返回用户通过手机App扫描该二维码手机App发送确认请求到服务器服务器通知Web端登录成功Web端完成登录流程qrcode-login/├── src/main/java/com/example/qrcodelogin/│ ├── QrcodeLoginApplication.java│ ├── config/│ │ ├── RedisConfig.java│ │ └── WebSocketConfig.java│ ├── controller/│ │ ├── LoginController.java│ │ └── QRCodeController.java│ ├── model/│ │ ├── QRCodeStatus.java│ │ └── UserInfo.java│ ├── service/│ │ ├── QRCodeService.java│ │ └── UserService.java│ └── util/│ └── JsonUtil.java├── src/main/resources/│ ├── application.properties│ └── static/│ ├── css/│ │ ├── login.css│ │ └── mobile.css│ ├── index.html│ └── mobile.html└── pom.xml

首先,创建一个 SpringBoot 项目,并添加必要的依赖:

4.0.0

org.Springframework.bootspring-boot-starter-parent2.7.0

com.exampleqrcode-login0.0.1-SNAPSHOTqrcode-loginSpringBoot QR code Login Demo

11

org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-websocketorg.springframework.bootspring-boot-starter-data-Rediscom.fasterxml.jackson.corejackson-databindcom.google.zxingcore3.5.1com.google.zxingjavase3.5.1org.projectlomboklomboktrue

org.springframework.bootspring-boot-maven-pluginorg.projectlomboklombok

在 application.yaml 中添加配置:

spring: redis: host: localhost port: 6379 password: database: 0 timeout: 5000ms lettuce: pool: max-active: 20 max-idle: 10 min-idle: 5qrcode: expire: seconds: 300 width: 100 height: 100package 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); } }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 redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate template = new RedisTemplate; template.setConnectionFactory(connectionFactory);ObjectMapper objectMapper = new ObjectMapper; objectMapper.activateDefaultTyping( objectMapper.getPolymorphicTypeValidator, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY );Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); jackson2JsonRedisSerializer.setObjectMapper(objectMapper);template.setKeySerializer(new StringRedisSerializer); template.setValueSerializer(jackson2JsonRedisSerializer); template.setHashKeySerializer(new StringRedisSerializer); template.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet; return template; } }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; } }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 { private static final Map 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 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); 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)); } 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); } } } }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; 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 fromJson(String json, Class clazz) { try { return objectMapper.readValue(json, clazz); } catch (JsonProcessingException e) { log.error("Convert json to object failed", e); return null; } } }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 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 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); 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); Map 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); Map 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); Map 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 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 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 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 getAllUsers { return USER_DB; } public UserInfo login(String userId) { UserInfo userInfo = USER_DB.get(userId); if (userInfo != null) { String token = UUID.randomUUID.toString; userInfo.setToken(token); return userInfo; } return null; } public UserInfo validateToken(String token) { for (UserInfo user : USER_DB.values) { if (token != null && token.equals(user.getToken)) { return user; } } return null; } }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 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 getQRCodeImage(@PathVariable String qrCodeId, HttpServletRequest request) { 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 scanQRCode(@RequestBody Map 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 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 cancelLogin(@RequestBody Map 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 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; @PostMapping("/validate") public ResponseEntity validateToken(@RequestBody Map 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> getTestUsers { return ResponseEntity.ok(userService.getAllUsers); } }

在 src/main/resources/static/index.html 中创建 Web 端登录页面:

扫码登录示例

扫码登录

请使用手机扫描二维码登录

欢迎回来

退出登录

如果您没有移动端演示 App,可以点击这里打开移动端模拟页面

扫码登录演示系统

这是一个基于 SpringBoot + WebSocket 的扫码登录演示系统。

技术栈:

后端:SpringBoot + WebSocket + Redis前端:纯原生 HTML/JS

演示流程:

打开"移动端模拟页面"在网页端显示二维码使用移动端模拟页面扫描二维码在移动端确认登录网页端自动登录成功let qrCodeId = ''; let webSocket = null; let refreshTimer = null; const qrcodeArea = document.getElementById('qrcode-area'); const qrcodeImg = document.getElementById('qrcode-img'); const qrcodeTip = document.getElementById('qrcode-tip'); const loginSuccess = document.getElementById('login-success'); const userAvatar = document.getElementById('user-avatar'); const userWelcome = document.getElementById('user-welcome'); const userEmail = document.getElementById('user-email'); const logoutBtn = document.getElementById('logout-btn'); window.addEventListener('load', generateQRCode); logoutBtn.addEventListener('click', logout); async function generateQRCode { try { const response = await fetch('/api/qrcode/generate'); if (!response.ok) { throw new Error('Failed to generate QR code'); } const data = await response.json; qrCodeId = data.qrCodeId; qrcodeImg.src = `/api/qrcode/image/${qrCodeId}`; qrcodeTip.textContent = '请使用手机扫描二维码登录'; qrcodeTip.className = 'qrcode-tip'; qrcodeArea.style.display = 'block'; loginSuccess.style.display = 'none'; connectWebSocket; if (refreshTimer) { clearTimeout(refreshTimer); } refreshTimer = setTimeout(refreshQRCode, 120000); } catch (error) { console.error('Error generating QR code:', error); qrcodeTip.textContent = '生成二维码失败,请刷新页面重试'; qrcodeTip.className = 'qrcode-tip expired'; } } function refreshQRCode { if (webSocket) { webSocket.close; webSocket = null; } generateQRCode; } function connectWebSocket { if (webSocket) { webSocket.close; } const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}/ws/qrcode`; webSocket = new WebSocket(wsUrl); webSocket.onopen = function { console.log('WebSocket connected'); const message = { qrCodeId: qrCodeId }; webSocket.send(JSON.stringify(message)); }; webSocket.onmessage = function(event) { const message = JSON.parse(event.data); console.log('Received message:', message); if (message.type === 'STATUS_CHANGE') { handleStatusChange(message); } }; webSocket.onerror = function(error) { console.error('WebSocket error:', error); }; webSocket.onclose = function { console.log('WebSocket disconnected'); }; } function handleStatusChange(message) { const status = message.status; switch (status) { case 'SCANNED': qrcodeTip.textContent = '已扫描,请在手机上确认'; qrcodeTip.className = 'qrcode-tip scanned'; break; case 'CONFIRMED': if (message.userInfo) { showLoginSuccess(message.userInfo); if (refreshTimer) { clearTimeout(refreshTimer); refreshTimer = null; } } break; case 'CANCELLED': case 'EXPIRED': qrcodeTip.textContent = '二维码已失效,请点击刷新'; qrcodeTip.className = 'qrcode-tip expired'; qrcodeTip.innerHTML = '二维码已失效,请刷新'; break; } } function showLoginSuccess(userInfo) { userAvatar.src = userInfo.avatar; userWelcome.textContent = `欢迎回来,${userInfo.username}`; userEmail.textContent = userInfo.email; qrcodeArea.style.display = 'none'; loginSuccess.style.display = 'block'; localStorage.setItem('userInfo', JSON.stringify(userInfo)); if (webSocket) { webSocket.close; webSocket = null; } } function logout { localStorage.removeItem('userInfo'); refreshQRCode; } (function checkLocalStorage { const storedUserInfo = localStorage.getItem('userInfo'); if (storedUserInfo) { try { const userInfo = JSON.parse(storedUserInfo); showLoginSuccess(userInfo); } catch (e) { console.error('Failed to parse user info:', e); localStorage.removeItem('userInfo'); } } });

在 src/main/resources/static/mobile.html 中创建移动端模拟页面:

移动端扫码登录

移动端扫码登录

请使用摄像头扫描二维码

或直接输入二维码 ID:

确认

已扫描到二维码

选择一个账号登录:

取消

确认在网页端登录该账号?

取消确认登录

登录成功

您已成功在网页端登录账号

返回

这是一个移动端 App 的模拟页面

// DOM 元素 const scanArea = document.getElementById('scan-area') const userSelectArea = document.getElementById('user-select-area') const loginConfirmArea = document.getElementById('login-confirm-area') const loginSuccessArea = document.getElementById('login-success-area') const qrcodeInput = document.getElementById('qrcode-input') const scanBtn = document.getElementById('scan-btn') const cancelScanBtn = document.getElementById('cancel-scan-btn') const cancelConfirmBtn = document.getElementById('cancel-confirm-btn') const confirmLoginBtn = document.getElementById('confirm-login-btn') const resetBtn = document.getElementById('reset-btn') const scannedQrcodeId = document.getElementById('scanned-qrcode-id') const userList = document.getElementById('user-list') const selectedUserAvatar = document.getElementById('selected-user-avatar') const selectedUserName = document.getElementById('selected-user-name') const selectedUserEmail = document.getElementById('selected-user-email') // 全局变量 let currentQrCodeId = '' let selectedUserId = '' let availableUsers = {} // 初始化 window.addEventListener('load', init) // 按钮事件 scanBtn.addEventListener('click', => scanQRCode(qrcodeInput.value)) cancelScanBtn.addEventListener('click', cancelScan) cancelConfirmBtn.addEventListener('click', cancelConfirm) confirmLoginBtn.addEventListener('click', confirmLogin) resetBtn.addEventListener('click', resetAll) // 初始化函数 function init { // 从 URL 获取二维码 ID const urlParams = new URLSearchParams(window.location.search) const qrCodeId = urlParams.get('qrCodeId') if (qrCodeId) { scanQRCode(qrCodeId) } // 获取可用用户 fetchAvailableUsers } // 获取可用用户 async function fetchAvailableUsers { try { const response = await fetch('/api/auth/users') if (!response.ok) { throw new Error('Failed to fetch users') } availableUsers = await response.json // 清空用户列表 userList.innerHTML = '' // 添加用户到列表 for (const userId in availableUsers) { const user = availableUsers[userId] const userItem = document.createElement('div') userItem.className = 'user-item' userItem.addEventListener('click', => selectUser(userId)) const userAvatar = document.createElement('div') userAvatar.className = 'user-avatar' const img = document.createElement('img') img.src = user.avatar img.alt = user.username const userName = document.createElement('div') userName.className = 'user-name' userName.textContent = user.username userAvatar.appendChild(img) userItem.appendChild(userAvatar) userItem.appendChild(userName) userList.appendChild(userItem) } } catch (error) { console.error('Error fetching users:', error) alert('获取用户列表失败,请刷新页面重试') } } // 扫描二维码 async function scanQRCode(qrCodeId) { if (!qrCodeId) { alert('请输入二维码 ID') return } try { const response = await fetch('/api/qrcode/scan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ qrCodeId: qrCodeId }) }) if (!response.ok) { throw new Error('Failed to scan QR code') } // 保存当前二维码 ID currentQrCodeId = qrCodeId // 显示扫描结果 scannedQrcodeId.textContent = `ID: ${qrCodeId}` // 切换界面 scanArea.style.display = 'none' userSelectArea.style.display = 'block' loginConfirmArea.style.display = 'none' loginSuccessArea.style.display = 'none' } catch (error) { console.error('Error scanning QR code:', error) alert('二维码无效或已过期') } } // 选择用户 function selectUser(userId) { selectedUserId = userId const user = availableUsers[userId] // 更新选中用户信息 selectedUserAvatar.src = user.avatar selectedUserName.textContent = user.username selectedUserEmail.textContent = user.email // 切换界面 scanArea.style.display = 'none' userSelectArea.style.display = 'none' loginConfirmArea.style.display = 'block' loginSuccessArea.style.display = 'none' } // 确认登录 async function confirmLogin { try { const response = await fetch('/api/qrcode/confirm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ qrCodeId: currentQrCodeId, userId: selectedUserId }) }) if (!response.ok) { throw new Error('Failed to confirm login') } // 切换界面 scanArea.style.display = 'none' userSelectArea.style.display = 'none' loginConfirmArea.style.display = 'none' loginSuccessArea.style.display = 'block' } catch (error) { console.error('Error confirming login:', error) alert('确认登录失败,请重试') } } // 取消扫描 async function cancelScan { try { await fetch('/api/qrcode/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ qrCodeId: currentQrCodeId }) }) } catch (error) { console.error('Error cancelling scan:', error) } resetAll } // 取消确认 function cancelConfirm { selectedUserId = '' // 切换界面 scanArea.style.display = 'none' userSelectArea.style.display = 'block' loginConfirmArea.style.display = 'none' loginSuccessArea.style.display = 'none' } // 重置所有状态 function resetAll { currentQrCodeId = '' selectedUserId = '' qrcodeInput.value = '' // 切换界面 scanArea.style.display = 'block' userSelectArea.style.display = 'none' loginConfirmArea.style.display = 'none' loginSuccessArea.style.display = 'none' }

在 src/main/resources/static/css/login.css 中添加 Web 端样式:

* { margin: 0; padding: 0; box-sizing: border-box;}body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f0f2f5; color: #333; }.login-container { display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px; }.login-box { flex: 1; max-width: 400px; background-color: #fff; border-radius: 10px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); padding: 40px; display: flex; flex-direction: column; }.login-header { text-align: center; margin-bottom: 30px; }.login-header h2 { font-size: 24px; color: #333; font-weight: 600; }.login-body { flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; }.qrcode-area { text-align: center; }.qrcode { width: 210px; height: 210px; margin: 0 auto 20px; padding: 5px; border: 1px solid #e0e0e0; border-radius: 10px; overflow: hidden; }.qrcode img { width: 100%; height: 100%; object-fit: contain; }.qrcode-tip { font-size: 14px; color: #666; margin-top: 15px; }.qrcode-tip.scanned { color: #1890ff; }.qrcode-tip.expired { color: #ff4d4f; }.qrcode-tip a { color: #1890ff; text-decoration: none; }.login-success { text-align: center; }.avatar { width: 100px; height: 100px; margin: 0 auto 20px; border-radius: 50%; overflow: hidden; border: 2px solid #1890ff; }.avatar img { width: 100%; height: 100%; object-fit: cover; }.welcome h3 { font-size: 20px; margin-bottom: 5px; color: #333; }.welcome p { font-size: 14px; color: #666; margin-bottom: 20px; }.logout button { background-color: #f0f0f0; border: none; padding: 8px 20px; border-radius: 4px; cursor: pointer; color: #333; font-size: 14px; transition: all 0.2s; }.logout button:hover { background-color: #e0e0e0; }.login-footer { margin-top: 30px; text-align: center; font-size: 13px; color: #999; }.login-footer a { color: #1890ff; text-decoration: none; }.login-info { flex: 1; max-width: 400px; margin-left: 20px; padding: 40px; background-color: #1890ff; color: white; border-radius: 10px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); }.login-info h3 { font-size: 22px; margin-bottom: 20px; }.login-info p { margin-bottom: 15px; font-size: 15px; }.login-info ul, .login-info ol { margin-left: 20px; margin-bottom: 15px; }.login-info li { margin-bottom: 8px; }@media (max-width: 768px) { .login-container { flex-direction: column; padding: 0; } .login-box { width: 100%; max-width: none; border-radius: 0; } .login-info { width: 100%; max-width: none; margin-left: 0; margin-top: 20px; border-radius: 0; } }

在 src/main/resources/static/css/mobile.css 中添加移动端样式:

* { margin: 0; padding: 0; box-sizing: border-box;}body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f0f2f5; color: #333; display: flex; justify-content: center; align-items: center; min-height: 100vh; }.mobile-container { width: 360px; max-width: 100%; background-color: #fff; border-radius: 10px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); overflow: hidden; }.mobile-header { background-color: #1890ff; color: white; padding: 15px; text-align: center; }.mobile-header h2 { font-size: 18px; font-weight: 500; }.mobile-body { padding: 20px; min-height: 400px; display: flex; flex-direction: column; justify-content: center; }.scan-area { text-align: center; }.scan-icon { width: 120px; height: 120px; margin: 0 auto 20px; background-color: #f0f0f0; border-radius: 10px; position: relative; }.scan-icon:before { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 60px; height: 60px; background-color: #1890ff; border-radius: 50%; opacity: 0.2; }.scan-icon:after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 40px; height: 40px; background-color: #1890ff; border-radius: 50%; }.scan-area p { margin-bottom: 20px; color: #666; }.scan-input { margin-top: 30px; text-align: center; }.scan-input p { font-size: 14px; margin-bottom: 10px; color: #999; }.scan-input input { width: 100%; padding: 10px; border: 1px solid #d9d9d9; border-radius: 4px; margin-bottom: 10px; }.scan-input button { width: 100%; padding: 10px; background-color: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; }.scan-result h3 { text-align: center; margin-bottom: 10px; color: #1890ff; }.scan-result p { text-align: center; margin-bottom: 20px; color: #666; word-break: break-all; }.user-select { margin: 20px 0; }.user-select p { text-align: center; margin-bottom: 15px; color: #333; }.user-list { display: flex; justify-content: center; gap: 20px; }.user-item { text-align: center; cursor: pointer; padding: 10px; border-radius: 8px; transition: all 0.2s; }.user-item:hover { background-color: #f0f0f0; }.user-avatar { width: 60px; height: 60px; margin: 0 auto 10px; border-radius: 50%; overflow: hidden; border: 2px solid #e0e0e0; }.user-avatar img { width: 100%; height: 100%; object-fit: cover; }.user-name { font-size: 14px; color: #333; }.scan-actions { margin-top: 30px; text-align: center; }.cancel-btn { padding: 8px 20px; background-color: #f0f0f0; border: none; border-radius: 4px; color: #333; cursor: pointer; }.login-confirm { text-align: center; }.login-user { display: flex; align-items: center; padding: 15px; background-color: #f9f9f9; border-radius: 8px; margin-bottom: 20px; }.login-user .user-avatar { width: 50px; height: 50px; margin: 0 15px 0 0; }.login-user .user-info { text-align: left; }.login-user .user-info h3 { font-size: 16px; margin-bottom: 5px; }.login-user .user-info p { font-size: 13px; color: #666; }.confirm-tip { margin: 20px 0; }.confirm-tip p { font-size: 16px; color: #333; }.confirm-actions { display: flex; justify-content: space-between; margin-top: 30px; }.confirm-btn { flex: 1; margin-left: 10px; padding: 10px; background-color: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; }.login-success { text-align: center; }.success-icon { width: 80px; height: 80px; margin: 0 auto 20px; background-color: #52c41a; border-radius: 50%; position: relative; }.success-icon:before { content: ''; position: absolute; top: 50%; left: 50%; width: 40px; height: 20px; border: 4px solid white; border-top: none; border-right: none; transform: translate(-50%, -60%) rotate(-45deg); }.login-success h3 { font-size: 20px; color: #52c41a; margin-bottom: 10px; }.login-success p { color: #666; margin-bottom: 30px; }.reset-btn { padding: 8px 20px; background-color: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; }.mobile-footer { padding: 15px; text-align: center; border-top: 1px solid #f0f0f0; }.mobile-footer p { font-size: 13px; color: #999; }五、运行项目

完成上述代码实现后,可以按照以下步骤运行项目:

首先,确保你已经安装了 Redis 并启动服务。可以使用 Docker 快速启动 Redis:

mvn clean packagejava -jar target/qrcode-login-0.0.1-SNAPSHOT.jar

或者直接通过 IDE 运行 QrcodeLoginApplication 类。

注意,本DEMO后端服务需要与移动设备在同一个局域网下

打开浏览器,访问 http://192.168.1.101:8080 进入Web端登录页面在另一个浏览器窗口或标签页中打开 http://192.168.1.101:8080/mobile.html 模拟移动端App,或者使用移动设备扫码二维码在移动端页面中输入二维码ID或直接点击Web端页面提供的链接按照界面提示完成扫码登录流程七、安全性考虑

来源:墨码行者

相关推荐