SpringBoot 實戰:掃碼登錄全流程解析,輕松搞定多端免密認證!
掃碼登錄是現代應用中一種常見且便捷的登錄方式。本篇文章將基于Spring Boot實現掃碼登錄的完整流程,涵蓋二維碼生成、掃碼確認、登錄狀態管理等關鍵功能,前后端結合示例助你快速上手。
目錄
- 項目結構與依賴配置
- 核心實體類設計
- 二維碼控制器 QRCodeController
- 二維碼服務 QRCodeService
- 用戶服務 UserService
- 登錄控制器 LoginController
- 手機端掃碼確認頁面示例
- PC端二維碼展示與輪詢思路(簡述)
項目結構與依賴配置
com.icoderoad
├── controller
│ ├── QRCodeController.java
│ └── LoginController.java
├── model
│ ├── QRCodeStatus.java
│ └── UserInfo.java
├── service
│ ├── QRCodeService.java
│ └── UserService.java
└── resources
├── templates
│ └── scan.html (手機掃碼確認頁面)
└── application.yml
pom.xml中需包含:
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- ZXing二維碼生成庫 -->
<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>
</dependencies>
核心實體類設計
QRCodeStatus.java
package com.icoderoad.model;
import lombok.Data;
@Data
public class QRCodeStatus {
public enum Status {
NEW, // 新生成,未掃描
SCANNED, // 已掃描
CONFIRMED, // 確認登錄
CANCELLED // 已取消
}
private String qrCodeId;
private Status status;
private UserInfo userInfo; // 登錄確認后綁定用戶信息
}
UserInfo.java
package com.icoderoad.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
private String userId;
private String username;
}
二維碼控制器QRCodeController.java
package com.icoderoad.controller;
import com.icoderoad.model.QRCodeStatus;
import com.icoderoad.model.UserInfo;
import com.icoderoad.service.QRCodeService;
import com.icoderoad.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) {
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.Status.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;
}
}
二維碼服務 QRCodeService.java
package com.icoderoad.service;
import com.icoderoad.model.QRCodeStatus;
import com.icoderoad.model.UserInfo;
import com.google.zxing.BarcodeFormat;
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 org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class QRCodeService {
private final Map<String, QRCodeStatus> qrCodeStatusMap = new ConcurrentHashMap<>();
/**
* 生成新的二維碼ID和狀態
*/
public QRCodeStatus generateQRCode() {
String qrCodeId = UUID.randomUUID().toString();
QRCodeStatus status = new QRCodeStatus();
status.setQrCodeId(qrCodeId);
status.setStatus(QRCodeStatus.Status.NEW);
qrCodeStatusMap.put(qrCodeId, status);
return status;
}
/**
* 根據二維碼ID生成二維碼圖片字節
*/
public byte[] generateQRCodeImage(String qrCodeId, String baseUrl) {
String qrContent = baseUrl + "/mobile/scan?qrCodeId=" + qrCodeId;
QRCodeWriter writer = new QRCodeWriter();
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
BitMatrix bitMatrix = writer.encode(qrContent, BarcodeFormat.QR_CODE, 250, 250);
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", baos);
return baos.toByteArray();
} catch (WriterException | IOException e) {
e.printStackTrace();
return null;
}
}
/**
* 更新二維碼狀態
*/
public boolean updateQRCodeStatus(String qrCodeId, QRCodeStatus.Status status) {
QRCodeStatus qrCodeStatus = qrCodeStatusMap.get(qrCodeId);
if (qrCodeStatus == null) return false;
qrCodeStatus.setStatus(status);
return true;
}
/**
* 確認登錄,綁定用戶信息
*/
public boolean confirmLogin(String qrCodeId, UserInfo userInfo) {
QRCodeStatus qrCodeStatus = qrCodeStatusMap.get(qrCodeId);
if (qrCodeStatus == null || qrCodeStatus.getStatus() != QRCodeStatus.Status.SCANNED) return false;
qrCodeStatus.setStatus(QRCodeStatus.Status.CONFIRMED);
qrCodeStatus.setUserInfo(userInfo);
return true;
}
/**
* 取消登錄
*/
public boolean cancelLogin(String qrCodeId) {
QRCodeStatus qrCodeStatus = qrCodeStatusMap.get(qrCodeId);
if (qrCodeStatus == null) return false;
qrCodeStatus.setStatus(QRCodeStatus.Status.CANCELLED);
return true;
}
/**
* 獲取二維碼狀態
*/
public QRCodeStatus getQRCodeStatus(String qrCodeId) {
return qrCodeStatusMap.get(qrCodeId);
}
}
用戶服務 UserService.java
package com.icoderoad.service;
import com.icoderoad.model.UserInfo;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserService {
private final Map<String, UserInfo> userMap = new HashMap<>();
public UserService() {
userMap.put("user1", new UserInfo("user1", "Alice"));
userMap.put("user2", new UserInfo("user2", "Bob"));
}
/**
* 模擬登錄:根據用戶ID獲取用戶信息
*/
public UserInfo login(String userId) {
return userMap.get(userId);
}
/**
* 驗證token,簡易模擬,token即為userId
*/
public UserInfo validateToken(String token) {
return userMap.get(token);
}
/**
* 獲取所有測試用戶
*/
public Map<String, UserInfo> getAllUsers() {
return userMap;
}
}
登錄控制器 LoginController.java
package com.icoderoad.controller;
import com.icoderoad.model.UserInfo;
import com.icoderoad.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/login")
public class LoginController {
@Autowired
private UserService userService;
/**
* 模擬用戶登錄,返回token(這里直接用userId代替token)
*/
@PostMapping
public String login(@RequestParam String userId) {
UserInfo user = userService.login(userId);
if (user == null) {
return "登錄失敗,用戶不存在";
}
// 這里可改為JWT等token
return user.getUserId();
}
}
手機端掃碼確認頁面示例(scan.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>掃碼確認登錄</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body class="p-3">
<h3>掃碼確認登錄</h3>
<div id="qrCodeId" class="mb-3"></div>
<div class="mb-3">
<label for="userSelect" class="form-label">選擇用戶</label>
<select id="userSelect" class="form-select"></select>
</div>
<button id="confirmBtn" class="btn btn-success me-2">確認登錄</button>
<button id="cancelBtn" class="btn btn-danger">取消登錄</button>
<div id="msg" class="mt-3"></div>
<script>
const urlParams = new URLSearchParams(window.location.search);
const qrCodeId = urlParams.get('qrCodeId');
document.getElementById('qrCodeId').innerText = "二維碼ID:" + qrCodeId;
const userSelect = document.getElementById('userSelect');
const msgDiv = document.getElementById('msg');
// 模擬請求獲取所有用戶
const users = {
"user1": "Alice",
"user2": "Bob"
};
for (const [id, name] of Object.entries(users)) {
const option = document.createElement('option');
option.value = id;
option.textContent = name;
userSelect.appendChild(option);
}
document.getElementById('confirmBtn').onclick = () => {
const userId = userSelect.value;
axios.post('/api/qrcode/confirm', {qrCodeId, userId})
.then(res => {
msgDiv.innerHTML = `<div class="alert alert-success">${res.data}</div>`;
})
.catch(err => {
msgDiv.innerHTML = `<div class="alert alert-danger">確認失敗: ${err.response.data}</div>`;
});
};
document.getElementById('cancelBtn').onclick = () => {
axios.post('/api/qrcode/cancel', {qrCodeId})
.then(res => {
msgDiv.innerHTML = `<div class="alert alert-warning">${res.data}</div>`;
})
.catch(err => {
msgDiv.innerHTML = `<div class="alert alert-danger">取消失敗: ${err.response.data}</div>`;
});
};
</script>
</body>
</html>
訪問該頁面示例:http://localhost:8080/mobile/scan?qrCodeId=xxx-xxx-xxx
PC端二維碼展示與輪詢思路簡述
- PC端訪問 /api/qrcode/generate 生成二維碼ID
- PC端通過 /api/qrcode/image/{qrCodeId} 獲取二維碼圖片并展示
- 手機端掃碼后訪問 /api/qrcode/scan 告訴服務器已掃描
- 手機端確認登錄后訪問 /api/qrcode/confirm 完成登錄綁定用戶信息
- PC端通過輪詢 /api/qrcode/status/{qrCodeId} 獲取二維碼登錄狀態變化,及時反饋用戶登錄結果
總結
本文詳細介紹了基于Spring Boot實現掃碼登錄的完整流程,包括二維碼生成、掃碼確認、狀態管理等核心邏輯。示例代碼清晰,服務層職責分明,便于擴展和維護.