本文目录导读:

我来详细介绍二维码扫码登录的实现方案,包含完整的Java实现代码。
扫码登录基本原理
核心流程
sequenceDiagram
participant Web端
participant 服务器
participant 手机端
Web端->>服务器: 1. 请求生成二维码
服务器->>服务器: 2. 生成唯一码(如UUID)
服务器->>Web端: 3. 返回二维码图片
Web端->>Web端: 4. 显示二维码
手机端->>服务器: 5. 扫描二维码
手机端->>服务器: 6. 确认登录
服务器->>Web端: 7. 轮询/推送登录状态
Web端->>Web端: 8. 登录成功,跳转
状态流转
待扫描 -> 已扫描 -> 已确认 -> 登录成功
↓ ↓ ↓
未登录 等待确认 登录成功
完整实现代码
Maven依赖
<!-- 二维码生成 -->
<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>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
核心实体类
@Data
public class QRCodeLoginDTO {
private String qrCodeId; // 二维码唯一标识
private String ticket; // 临时凭证
private Integer status; // 状态:0-待扫描,1-已扫描,2-已确认
private String userId; // 用户ID
private Long expireTime; // 过期时间
}
public enum QRCodeStatus {
WAITING(0, "待扫描"),
SCANNED(1, "已扫描"),
CONFIRMED(2, "已确认"),
EXPIRED(3, "已过期");
private int code;
private String desc;
// getter方法...
}
二维码生成服务
@Service
@Slf4j
public class QRCodeService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Value("${qr.code.expire:300}")
private long expireSeconds; // 默认5分钟
@Value("${qr.code.base-url}")
private String baseUrl;
/**
* 生成二维码
*/
public String generateQRCode() throws Exception {
// 1. 生成唯一ID
String qrCodeId = UUID.randomUUID().toString().replace("-", "");
// 2. 生成临时凭证(防篡改)
String ticket = generateTicket(qrCodeId);
// 3. 构建二维码内容(URL格式:包含ID和凭证)
String content = String.format("%s/qr/login?qrCodeId=%s&ticket=%s",
baseUrl, qrCodeId, ticket);
// 4. 生成二维码图片
String image = generateQRCodeImage(content);
// 5. 保存登录信息到Redis
QRCodeLoginDTO loginDTO = new QRCodeLoginDTO();
loginDTO.setQrCodeId(qrCodeId);
loginDTO.setTicket(ticket);
loginDTO.setStatus(QRCodeStatus.WAITING.getCode());
loginDTO.setExpireTime(System.currentTimeMillis() + expireSeconds * 1000);
redisTemplate.opsForValue().set(
"qr:login:" + qrCodeId,
loginDTO,
expireSeconds,
TimeUnit.SECONDS
);
return image;
}
/**
* 生成二维码图片(Base64)
*/
private String generateQRCodeImage(String content) throws Exception {
int width = 300;
int height = 300;
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix bitMatrix = new QRCodeWriter().encode(
content,
BarcodeFormat.QR_CODE,
width,
height,
hints
);
ByteArrayOutputStream out = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", out);
return Base64.getEncoder().encodeToString(out.toByteArray());
}
/**
* 生成临时凭证
*/
private String generateTicket(String qrCodeId) {
String raw = qrCodeId + System.currentTimeMillis() + "secret_key";
return DigestUtils.md5DigestAsHex(raw.getBytes());
}
}
扫码登录控制器
@RestController
@RequestMapping("/api/qr")
@Slf4j
public class QRCodeController {
@Autowired
private QRCodeService qrCodeService;
@Autowired
private LoginService loginService;
@Autowired
private WebSocketService webSocketService;
/**
* 获取二维码
*/
@GetMapping("/get")
public Result<Map<String, Object>> getQRCode() {
try {
String base64Image = qrCodeService.generateQRCode();
Map<String, Object> result = new HashMap<>();
result.put("qrImage", "data:image/png;base64," + base64Image);
result.put("expireTime", 300); // 过期时间(秒)
return Result.success(result);
} catch (Exception e) {
log.error("生成二维码失败", e);
return Result.error("生成二维码失败");
}
}
/**
* 手机端扫码
*/
@PostMapping("/scan")
public Result<Boolean> scanQRCode(@RequestBody QRCodeScanDTO scanDTO) {
// 1. 验证二维码是否有效
QRCodeLoginDTO loginInfo = qrCodeService.getLoginInfo(scanDTO.getQrCodeId());
if (loginInfo == null) {
return Result.error("二维码已过期");
}
// 2. 验证凭证
if (!loginInfo.getTicket().equals(scanDTO.getTicket())) {
return Result.error("非法请求");
}
// 3. 更新状态为已扫描
loginInfo.setStatus(QRCodeStatus.SCANNED.getCode());
qrCodeService.updateLoginInfo(loginInfo);
// 4. 通过WebSocket通知网页端
webSocketService.sendMessage(scanDTO.getQrCodeId(),
new QRCodeMessage(QRCodeStatus.SCANNED.getCode(), "已扫描"));
return Result.success(true);
}
/**
* 手机端确认登录
*/
@PostMapping("/confirm")
public Result<String> confirmLogin(@RequestBody QRCodeConfirmDTO confirmDTO) {
// 1. 验证二维码信息
QRCodeLoginDTO loginInfo = qrCodeService.getLoginInfo(confirmDTO.getQrCodeId());
if (loginInfo == null || loginInfo.getStatus() != QRCodeStatus.SCANNED.getCode()) {
return Result.error("请先扫码");
}
// 2. 用户认证
String userId = loginService.authenticate(confirmDTO.getToken());
if (userId == null) {
return Result.error("用户认证失败");
}
// 3. 生成临时登录凭证
String tempToken = UUID.randomUUID().toString();
loginInfo.setUserId(userId);
loginInfo.setStatus(QRCodeStatus.CONFIRMED.getCode());
qrCodeService.updateLoginInfo(loginInfo);
// 4. 保存临时token到Redis(供网页端使用)
redisTemplate.opsForValue().set(
"qr:token:" + confirmDTO.getQrCodeId(),
tempToken,
60, // 1分钟有效
TimeUnit.SECONDS
);
// 5. 通知网页端登录成功
webSocketService.sendMessage(confirmDTO.getQrCodeId(),
new QRCodeMessage(QRCodeStatus.CONFIRMED.getCode(), "登录成功"));
return Result.success(tempToken);
}
/**
* 网页端轮询检查登录状态
*/
@GetMapping("/check/{qrCodeId}")
public Result<QRCodeCheckResult> checkLoginStatus(@PathVariable String qrCodeId) {
// 1. 检查临时token
String tempToken = (String) redisTemplate.opsForValue().get("qr:token:" + qrCodeId);
if (tempToken != null) {
// 2. 获取登录信息
QRCodeLoginDTO loginInfo = qrCodeService.getLoginInfo(qrCodeId);
if (loginInfo != null && loginInfo.getStatus() == QRCodeStatus.CONFIRMED.getCode()) {
// 3. 生成正式token
String accessToken = loginService.generateToken(loginInfo.getUserId());
// 4. 清理临时数据
redisTemplate.delete("qr:token:" + qrCodeId);
redisTemplate.delete("qr:login:" + qrCodeId);
return Result.success(QRCodeCheckResult.builder()
.status(QRCodeStatus.CONFIRMED.getCode())
.accessToken(accessToken)
.build());
}
}
// 5. 返回当前状态
QRCodeLoginDTO loginInfo = qrCodeService.getLoginInfo(qrCodeId);
if (loginInfo == null) {
return Result.success(QRCodeCheckResult.builder()
.status(QRCodeStatus.EXPIRED.getCode())
.build());
}
return Result.success(QRCodeCheckResult.builder()
.status(loginInfo.getStatus())
.build());
}
}
WebSocket推送服务
@Component
@Slf4j
public class WebSocketService {
private final Map<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();
/**
* 发送消息
*/
public void sendMessage(String qrCodeId, QRCodeMessage message) {
WebSocketSession session = sessionMap.get(qrCodeId);
if (session != null && session.isOpen()) {
try {
session.sendMessage(new TextMessage(JSON.toJSONString(message)));
} catch (IOException e) {
log.error("WebSocket发送消息失败", e);
}
}
}
/**
* 注册session
*/
public void register(String qrCodeId, WebSocketSession session) {
sessionMap.put(qrCodeId, session);
}
/**
* 移除session
*/
public void remove(String qrCodeId) {
sessionMap.remove(qrCodeId);
}
}
WebSocket处理器
@Component
public class QRCodeWebSocketHandler extends TextWebSocketHandler {
@Autowired
private WebSocketService webSocketService;
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String qrCodeId = getQRCodeId(session);
if (qrCodeId != null) {
webSocketService.register(qrCodeId, session);
log.info("WebSocket连接建立: {}", qrCodeId);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String qrCodeId = getQRCodeId(session);
if (qrCodeId != null) {
webSocketService.remove(qrCodeId);
log.info("WebSocket连接关闭: {}", qrCodeId);
}
}
private String getQRCodeId(WebSocketSession session) {
// 从URL参数中获取qrCodeId
URI uri = session.getUri();
if (uri != null) {
String query = uri.getQuery();
if (query != null) {
String[] params = query.split("&");
for (String param : params) {
String[] kv = param.split("=");
if (kv.length == 2 && "qrCodeId".equals(kv[0])) {
return kv[1];
}
}
}
}
return null;
}
}
WebSocket配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private QRCodeWebSocketHandler qrCodeWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(qrCodeWebSocketHandler, "/ws/qr")
.setAllowedOrigins("*");
}
}
前端HTML实现
<!DOCTYPE html>
<html>
<head>扫码登录</title>
</head>
<body>
<div id="qr-container">
<img id="qr-image" />
<div id="qr-status">等待扫描</div>
</div>
<script>
let qrCodeId;
let websocket;
// 1. 获取二维码
async function getQRCode() {
const response = await fetch('/api/qr/get');
const result = await response.json();
if (result.success) {
// 显示二维码
document.getElementById('qr-image').src = result.data.qrImage;
qrCodeId = result.data.qrCodeId;
// 建立WebSocket连接
connectWebSocket(qrCodeId);
// 开始轮询
startPolling(qrCodeId);
}
}
// 2. 建立WebSocket连接
function connectWebSocket(qrCodeId) {
const ws = new WebSocket(`ws://localhost:8080/ws/qr?qrCodeId=${qrCodeId}`);
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
handleQRCodeStatus(data);
};
ws.onclose = function() {
console.log('WebSocket连接关闭');
};
}
// 3. 处理状态变化
function handleQRCodeStatus(data) {
const statusMap = {
0: '等待扫描',
1: '已扫描,请在手机上确认',
2: '登录成功,正在跳转...'
};
document.getElementById('qr-status').textContent = statusMap[data.status] || '未知状态';
if (data.status === 2) {
// 登录成功,跳转首页
setTimeout(() => {
window.location.href = '/index';
}, 1000);
}
}
// 4. 轮询检查状态(作为WebSocket的补充)
function startPolling(qrCodeId) {
setInterval(async () => {
const response = await fetch(`/api/qr/check/${qrCodeId}`);
const result = await response.json();
if (result.success && result.data.status === 2) {
// 登录成功
localStorage.setItem('access_token', result.data.accessToken);
window.location.href = '/index';
}
}, 3000); // 每3秒轮询一次
}
// 初始化
getQRCode();
</script>
</body>
</html>
关键优化建议
安全性优化
// 添加防重复扫码
public synchronized boolean processScan(String qrCodeId, String userId) {
// 加锁防止并发
boolean locked = redisTemplate.opsForValue()
.setIfAbsent("qr:lock:" + qrCodeId, userId, 5, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("二维码正在被扫描");
}
try {
// 业务处理
} finally {
redisTemplate.delete("qr:lock:" + qrCodeId);
}
}
性能优化
// 使用连接池
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory factory = new LettuceConnectionFactory();
// 配置连接池
return factory;
}
// 异步处理
@Async
public CompletableFuture<Boolean> asyncProcessLogin(QRCodeLoginDTO loginDTO) {
// 异步处理登录
return CompletableFuture.completedFuture(true);
}
异常处理
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(QRCodeExpiredException.class)
public Result handleQRCodeExpired() {
return Result.error("二维码已过期,请刷新");
}
@ExceptionHandler(QRCodeInvalidException.class)
public Result handleQRCodeInvalid() {
return Result.error("非法二维码");
}
}
部署配置
application.yml
qr:
code:
expire: 300 # 二维码过期时间(秒)
base-url: http://your-domain.com
secret-key: your-secret-key
redis:
host: localhost
port: 6379
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-wait: -1
这个实现包含了完整的扫码登录流程,支持WebSocket实时推送和HTTP轮询两种模式,并考虑了安全性和性能优化。