SpringBoot 扫码登录全流程:UUID 生成、状态轮询、授权回调详解

引入依赖

bash 复制代码
<!-- 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.3</version>
</dependency>
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>javase</artifactId>
    <version>3.5.3</version>
</dependency>

<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

配置

bash 复制代码
spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0
  session:
    store-type: redis

生成二维码

bash 复制代码
/**
 * 二维码生成工具类
 */
public class QRCodeUtil {
    
    private static final int WIDTH = 300;
    private static final int HEIGHT = 300;
    private static final String FORMAT = "png";
    
    /**
     * 生成二维码字节数组
     * @param content 二维码内容
     * @return 二维码图片字节数组
     */
    public static byte[] generateQRCode(String content) throws WriterException, IOException {
        Map<EncodeHintType, Object> hints = new HashMap<>();
        // 设置字符编码
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        // 设置容错级别
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        // 设置边距
        hints.put(EncodeHintType.MARGIN, 1);
        
        BitMatrix bitMatrix = new MultiFormatWriter().encode(
                content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, hints);
        
        BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix);
        
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(image, FORMAT, outputStream);
        
        return outputStream.toByteArray();
    }
    
    /**
     * 生成二维码Base64字符串
     */
    public static String generateQRCodeBase64(String content) throws WriterException, IOException {
        byte[] bytes = generateQRCode(content);
        return "data:image/png;base64," + java.util.Base64.getEncoder().encodeToString(bytes);
    }
}

定义常量和实体类

bash 复制代码
/**
 * 常量类
 */
public class Constants {
    // Redis 中二维码状态的前缀
    public static final String QR_CODE_PREFIX = "qr:code:";
    // 二维码过期时间(秒)
    public static final long QR_CODE_EXPIRE = 5 * 60;
}

/**
 * 二维码状态枚举
 */
public enum QRCodeStatus {
    WAITING("waiting", "等待扫描"),
    SCANNED("scanned", "已扫描"),
    CONFIRMED("confirmed", "已确认"),
    CANCELLED("cancelled", "已取消"),
    EXPIRED("expired", "已过期"),
    ERROR("error", "错误");
    
    private String code;
    private String message;
    
    QRCodeStatus(String code, String message) {
        this.code = code;
        this.message = message;
    }
    
    // getter 方法
    public String getCode() {
        return code;
    }
    
    public String getMessage() {
        return message;
    }
}

/**
 * WebSocket消息实体
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WebSocketMessage {
    private String uuid;
    private QRCodeStatus status;
    private String message;
    private Object data;
}

/**
 * 用户信息实体
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
    private Long userId;
    private String username;
    private String nickname;
    private String avatar;
}

配置WebSocket

bash 复制代码
/**
 * WebSocket配置
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 启用简单消息代理,前缀为/topic的消息会被代理转发到订阅了相应主题的客户端
        config.enableSimpleBroker("/topic");
        // 客户端发送消息的前缀
        config.setApplicationDestinationPrefixes("/app");
    }
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册STOMP端点,客户端通过此端点连接到WebSocket服务器
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }
    
    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }
}

二维码服务实现

bash 复制代码
/**
 * 二维码服务
 */
@Service
public class QRCodeService {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 创建新的二维码
     * @return 二维码UUID
     */
    public String createQRCode() {
        String uuid = java.util.UUID.randomUUID().toString();
        String redisKey = Constants.QR_CODE_PREFIX + uuid;

        // 存储二维码状态到Redis
        redisTemplate.opsForHash().put(redisKey, "status", QRCodeStatus.WAITING.getCode());
        redisTemplate.expire(redisKey, Constants.QR_CODE_EXPIRE, TimeUnit.SECONDS);
        return uuid;
    }

    /**
     * 更新二维码状态
     * @param uuid 二维码UUID
     * @param status 新状态
     */
    public void updateQRCodeStatus(String uuid, QRCodeStatus status) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        redisTemplate.opsForHash().put(redisKey, "status", status.getCode());
        redisTemplate.expire(redisKey, getRemainingTime(uuid), TimeUnit.SECONDS);
    }

    /**
     * 更新二维码状态并关联用户信息
     * @param uuid 二维码UUID
     * @param status 新状态
     * @param userInfo 用户信息
     */
    public void updateQRCodeStatusWithUser(String uuid, QRCodeStatus status, UserInfo userInfo) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        // 使用Hash结构存储状态和用户信息
        redisTemplate.opsForHash().put(redisKey, "status", status.getCode());
        redisTemplate.opsForHash().put(redisKey, "userInfo", userInfo);
        redisTemplate.expire(redisKey, getRemainingTime(uuid), TimeUnit.SECONDS);
    }

    /**
     * 获取二维码状态
     * @param uuid 二维码UUID
     * @return 状态
     */
    public QRCodeStatus getQRCodeStatus(String uuid) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        Object status = redisTemplate.opsForHash().get(redisKey, "status");

        if (status == null) {
            return QRCodeStatus.EXPIRED;
        }

        for (QRCodeStatus qrCodeStatus : QRCodeStatus.values()) {
            if (qrCodeStatus.getCode().equals(status.toString())) {
                return qrCodeStatus;
            }
        }

        return QRCodeStatus.ERROR;
    }

    /**
     * 获取二维码关联的用户信息
     * @param uuid 二维码UUID
     * @return 用户信息
     */
    public UserInfo getUserInfo(String uuid) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        return (UserInfo) redisTemplate.opsForHash().get(redisKey, "userInfo");
    }

    /**
     * 获取Redis键的剩余时间
     * @param uuid 二维码UUID
     * @return 剩余时间(秒)
     */
    private Long getRemainingTime(String uuid) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        return redisTemplate.getExpire(redisKey, TimeUnit.SECONDS);
    }

    /**
     * 使二维码过期
     * @param uuid 二维码UUID
     */
    public void expireQRCode(String uuid) {
        String redisKey = Constants.QR_CODE_PREFIX + uuid;
        redisTemplate.delete(redisKey);
    }
}

后端控制器实现

bash 复制代码
/**
 * 登录控制器
 */
@RestController
@RequestMapping("/api/login")
public class LoginController {

    @Autowired
    private QRCodeService qrCodeService;

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    /**
     * 生成二维码
     */
    @GetMapping("/qrCode")
    public Map<String, Object> generateQRCode() throws Exception {
        String uuid = qrCodeService.createQRCode();
        String qrCodeBase64 = QRCodeUtil.generateQRCodeBase64(uuid);

        Map<String, Object> result = new HashMap<>();
        result.put("uuid", uuid);
        result.put("qrCode", qrCodeBase64);
        result.put("expireTime", Constants.QR_CODE_EXPIRE);

        return result;
    }

    /**
     * 处理扫码请求
     */
    @PostMapping("/scan")
    public Map<String, Object> scanQRCode(@RequestBody Map<String, String> request) {

        String uuid = request.get("uuid");
        Long userId = Long.parseLong(request.get("userId"));
        Map<String, Object> result = new HashMap<>();

        // 检查二维码是否存在且有效
        QRCodeStatus status = qrCodeService.getQRCodeStatus(uuid);
        if (status != QRCodeStatus.WAITING) {
            result.put("success", false);
            result.put("message", "二维码无效或已过期");
            return result;
        }

        // 获取用户信息(这里应该从数据库查询,简化示例)
        UserInfo userInfo = new UserInfo(userId, "一安", "一安未来", "https://picsum.photos/200/200");

        // 更新二维码状态为已扫描
        qrCodeService.updateQRCodeStatusWithUser(uuid, QRCodeStatus.SCANNED, userInfo);

        // 通过WebSocket通知前端二维码已被扫描
        WebSocketMessage message = new WebSocketMessage(
                uuid,
                QRCodeStatus.SCANNED,
                "二维码已被扫描,请确认登录",
                userInfo
        );
        messagingTemplate.convertAndSend("/topic/qr/" + uuid, message);

        result.put("success", true);
        result.put("message", "扫码成功,请在PC端确认登录");
        return result;
    }

    /**
     * 处理授权请求
     */
    @PostMapping("/authorize")
    public Map<String, Object> authorize(@RequestBody Map<String, String> request) {


        String uuid = request.get("uuid");
        Boolean confirm = Boolean.parseBoolean(request.get("confirm"));
        Map<String, Object> result = new HashMap<>();

        // 检查二维码是否存在且已扫描
        QRCodeStatus status = qrCodeService.getQRCodeStatus(uuid);
        if (status != QRCodeStatus.SCANNED) {
            result.put("success", false);
            result.put("message", "二维码状态无效");
            return result;
        }

        if (confirm) {
            // 获取用户信息
            UserInfo userInfo = qrCodeService.getUserInfo(uuid);

            // 更新二维码状态为已确认
            qrCodeService.updateQRCodeStatusWithUser(uuid, QRCodeStatus.CONFIRMED, userInfo);

            // 通过WebSocket通知前端登录成功
            WebSocketMessage message = new WebSocketMessage(
                    uuid,
                    QRCodeStatus.CONFIRMED,
                    "登录成功",
                    userInfo
            );
            messagingTemplate.convertAndSend("/topic/qr/" + uuid, message);

            result.put("success", true);
            result.put("message", "授权成功");
            result.put("userInfo", userInfo);
        } else {
            // 更新二维码状态为已取消
            qrCodeService.updateQRCodeStatus(uuid, QRCodeStatus.ERROR);

            // 通过WebSocket通知前端登录已取消
            WebSocketMessage message = new WebSocketMessage(
                    uuid,
                    QRCodeStatus.ERROR,
                    "用户取消登录",
                    null
            );
            messagingTemplate.convertAndSend("/topic/qr/" + uuid, message);

            result.put("success", false);
            result.put("message", "用户取消登录");
        }

        return result;
    }

    /**
     * 检查二维码状态(轮询方式)
     */
    @GetMapping("/checkStatus")
    public Map<String, Object> checkStatus(@RequestParam String uuid) {
        Map<String, Object> result = new HashMap<>();

        QRCodeStatus status = qrCodeService.getQRCodeStatus(uuid);
        result.put("status", status.getCode());
        result.put("message", status.getMessage());

        if (status == QRCodeStatus.SCANNED) {
            // 获取用户信息
            UserInfo userInfo = qrCodeService.getUserInfo(uuid);
            result.put("userInfo", userInfo);
        }

        return result;
    }
}

WebSocket消息处理器

bash 复制代码
/**
 * WebSocket消息处理器
 */
@Controller
public class WebSocketController {
    
    /**
     * 处理客户端订阅二维码状态的消息
     */
    @MessageMapping("/subscribeQr")
    @SendTo("/topic/qr/{uuid}")
    public WebSocketMessage subscribeQr(@PathVariable String uuid) {
        // 这里可以根据需要返回初始状态
        return new WebSocketMessage(
                uuid, 
                QRCodeStatus.WAITING, 
                "等待扫描", 
                null
        );
    }
}

二维码登录系统技术方案

流程概述

1. 二维码生成阶段

User WebFront WebBack Redis 打开登录页面 请求生成二维码ID 生成唯一二维码ID 存储二维码状态(等待扫描) 返回二维码图片 建立WebSocket连接 User WebFront WebBack Redis

  1. 扫描确认阶段

User MobileApp WebBack WebFront Redis 扫描二维码 发送扫描请求(二维码ID) 更新状态为已扫描 WebSocket推送状态变更 更新UI显示已扫描 显示账号选择界面 选择账号并确认 User MobileApp WebBack WebFront Redis

  1. 登录完成阶段

User MobileApp WebBack WebFront Redis 确认登录请求 验证二维码状态 生成用户令牌 更新状态为已确认(含用户信息) WebSocket推送登录成功 完成登录流程 显示用户信息 显示登录成功 User MobileApp WebBack WebFront Redis

数据结构设计

bash 复制代码
// 二维码状态枚举
enum QRCodeStatus {
    PENDING,      // 等待扫描
    SCANNED,      // 已扫描
    CONFIRMED,    // 已确认
    EXPIRED,      // 已过期
    REJECTED      // 已拒绝
}

// 二维码数据结构
public class QRCodeData {
    private String qrCodeId;          // 二维码ID
    private QRCodeStatus status;      // 当前状态
    private Long createTime;          // 创建时间
    private Long scanTime;           // 扫描时间
    private Long confirmTime;        // 确认时间
    private String userId;           // 用户ID(确认后)
    private String deviceInfo;       // 设备信息
    private String token;            // 登录令牌
    private String ipAddress;        // 扫描IP
}
bash 复制代码
@RestController
@RequestMapping("/api/qrcode")
public class QRCodeController {
    
    @PostMapping("/generate")
    public ResponseResult<QRCodeResponse> generateQRCode() {
        // 1. 生成唯一二维码ID
        String qrCodeId = UUID.randomUUID().toString();
        
        // 2. 创建二维码数据
        QRCodeData qrCodeData = new QRCodeData();
        qrCodeData.setQrCodeId(qrCodeId);
        qrCodeData.setStatus(QRCodeStatus.PENDING);
        qrCodeData.setCreateTime(System.currentTimeMillis());
        qrCodeData.setExpireTime(System.currentTimeMillis() + 5 * 60 * 1000); // 5分钟过期
        
        // 3. 存储到Redis
        redisTemplate.opsForValue().set(
            "qrcode:" + qrCodeId, 
            qrCodeData, 
            5, TimeUnit.MINUTES
        );
        
        // 4. 生成二维码图片
        String qrContent = generateQRContent(qrCodeId);
        byte[] qrImage = generateQRImage(qrContent);
        
        return ResponseResult.success(new QRCodeResponse(qrCodeId, qrImage));
    }
    
    @PostMapping("/scan")
    public ResponseResult<Void> scanQRCode(@RequestBody ScanRequest request) {
        // 1. 验证二维码存在且未过期
        QRCodeData qrCodeData = getQRCodeData(request.getQrCodeId());
        if (qrCodeData == null || qrCodeData.isExpired()) {
            return ResponseResult.error("二维码已过期");
        }
        
        // 2. 更新状态为已扫描
        qrCodeData.setStatus(QRCodeStatus.SCANNED);
        qrCodeData.setScanTime(System.currentTimeMillis());
        qrCodeData.setDeviceInfo(request.getDeviceInfo());
        qrCodeData.setIpAddress(request.getIpAddress());
        
        // 3. 保存到Redis
        redisTemplate.opsForValue().set(
            "qrcode:" + qrCodeId, 
            qrCodeData
        );
        
        // 4. 通过WebSocket通知Web端
        notifyWebSocket(qrCodeId, "SCANNED");
        
        return ResponseResult.success();
    }
    
    @PostMapping("/confirm")
    public ResponseResult<LoginResult> confirmLogin(@RequestBody ConfirmRequest request) {
        // 1. 验证二维码状态
        QRCodeData qrCodeData = getQRCodeData(request.getQrCodeId());
        if (qrCodeData.getStatus() != QRCodeStatus.SCANNED) {
            return ResponseResult.error("二维码状态无效");
        }
        
        // 2. 验证用户身份
        User user = authenticateUser(request.getToken());
        
        // 3. 生成登录令牌
        String loginToken = generateLoginToken(user);
        
        // 4. 更新二维码状态
        qrCodeData.setStatus(QRCodeStatus.CONFIRMED);
        qrCodeData.setConfirmTime(System.currentTimeMillis());
        qrCodeData.setUserId(user.getId());
        qrCodeData.setToken(loginToken);
        
        // 5. 保存到Redis
        redisTemplate.opsForValue().set(
            "qrcode:" + qrCodeId, 
            qrCodeData
        );
        
        // 6. 通过WebSocket通知Web端
        notifyWebSocket(qrCodeId, "CONFIRMED", user);
        
        return ResponseResult.success(new LoginResult(loginToken));
    }
}

WebSocket处理器

bash 复制代码
@Component
@ServerEndpoint("/ws/qrcode/{qrCodeId}")
public class QRCodeWebSocketHandler {
    
    private static Map<String, Session> sessions = new ConcurrentHashMap<>();
    
    @OnOpen
    public void onOpen(Session session, @PathParam("qrCodeId") String qrCodeId) {
        sessions.put(qrCodeId, session);
    }
    
    @OnClose
    public void onClose(@PathParam("qrCodeId") String qrCodeId) {
        sessions.remove(qrCodeId);
    }
    
    public void notifyStatusChange(String qrCodeId, String status, Object data) {
        Session session = sessions.get(qrCodeId);
        if (session != null && session.isOpen()) {
            try {
                Map<String, Object> message = new HashMap<>();
                message.put("event", "QRCODE_STATUS_CHANGE");
                message.put("qrCodeId", qrCodeId);
                message.put("status", status);
                message.put("data", data);
                message.put("timestamp", System.currentTimeMillis());
                
                session.getBasicRemote().sendText(
                    objectMapper.writeValueAsString(message)
                );
            } catch (Exception e) {
                log.error("WebSocket发送失败", e);
            }
        }
    }
}

安全增强实现

bash 复制代码
@Component
public class SecurityEnhancer {
    
    // 防重放攻击
    public boolean verifyReplayAttack(String signature, String timestamp, String nonce) {
        // 1. 检查时间戳是否在有效期内(5分钟内)
        long currentTime = System.currentTimeMillis();
        long requestTime = Long.parseLong(timestamp);
        if (Math.abs(currentTime - requestTime) > 5 * 60 * 1000) {
            return false;
        }
        
        // 2. 检查nonce是否已使用过
        if (redisTemplate.hasKey("nonce:" + nonce)) {
            return false;
        }
        redisTemplate.opsForValue().set("nonce:" + nonce, "1", 5, TimeUnit.MINUTES);
        
        // 3. 验证签名
        String expectedSignature = calculateSignature(timestamp, nonce);
        return signature.equals(expectedSignature);
    }
    
    // IP限制
    public boolean checkIpLimit(String ipAddress) {
        String key = "ip_limit:" + ipAddress;
        Long count = redisTemplate.opsForValue().increment(key, 1);
        
        if (count == 1) {
            redisTemplate.expire(key, 1, TimeUnit.HOURS);
        }
        
        return count <= 100; // 每小时最多100次请求
    }
    
    // 数据加密
    public String encryptData(String data) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            byte[] encrypted = cipher.doFinal(data.getBytes());
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("加密失败", e);
        }
    }
}

多设备支持实现

bash 复制代码
@Service
public class DeviceManagementService {
    
    // 设备管理
    public void recordDeviceLogin(String userId, DeviceInfo deviceInfo) {
        DeviceRecord record = new DeviceRecord();
        record.setUserId(userId);
        record.setDeviceId(deviceInfo.getDeviceId());
        record.setDeviceType(deviceInfo.getDeviceType());
        record.setLoginTime(System.currentTimeMillis());
        record.setLocation(deviceInfo.getLocation());
        record.setIpAddress(deviceInfo.getIpAddress());
        
        // 保存设备记录
        deviceRecordRepository.save(record);
        
        // 检查异地登录
        checkUnusualLocation(userId, deviceInfo.getLocation());
    }
    
    // 异地登录检测
    private void checkUnusualLocation(String userId, String newLocation) {
        List<DeviceRecord> recentLogins = deviceRecordRepository
            .findRecentLogins(userId, 30); // 最近30天
        
        if (recentLogins.isEmpty()) {
            return;
        }
        
        String commonLocation = findCommonLocation(recentLogins);
        if (!isSameRegion(commonLocation, newLocation)) {
            // 发送异地登录提醒
            sendUnusualLoginAlert(userId, newLocation);
        }
    }
    
    // 单点登录
    public void handleSingleSignOn(String userId, String newToken) {
        // 1. 使旧token失效
        invalidateOldTokens(userId);
        
        // 2. 保存新token
        saveNewToken(userId, newToken);
        
        // 3. 通知其他设备
        notifyOtherDevices(userId, "NEW_LOGIN_DETECTED");
    }
}

前端实现

Web端二维码处理

bash 复制代码
class QRCodeLogin {
    constructor() {
        this.qrCodeId = null;
        this.ws = null;
        this.pollingInterval = null;
    }
    
    // 初始化二维码
    async initQRCode() {
        try {
            const response = await fetch('/api/qrcode/generate');
            const data = await response.json();
            
            this.qrCodeId = data.qrCodeId;
            this.displayQRCode(data.qrImage);
            this.connectWebSocket();
            
            // 启动状态轮询(备用方案)
            this.startStatusPolling();
        } catch (error) {
            console.error('生成二维码失败:', error);
        }
    }
    
    // WebSocket连接
    connectWebSocket() {
        this.ws = new WebSocket(`ws://${location.host}/ws/qrcode/${this.qrCodeId}`);
        
        this.ws.onmessage = (event) => {
            const message = JSON.parse(event.data);
            this.handleStatusChange(message);
        };
        
        this.ws.onclose = () => {
            console.log('WebSocket连接关闭,切换到轮询模式');
            this.switchToPolling();
        };
    }
    
    // 处理状态变更
    handleStatusChange(message) {
        switch (message.status) {
            case 'SCANNED':
                this.showScannedStatus();
                break;
            case 'CONFIRMED':
                this.completeLogin(message.data);
                break;
            case 'EXPIRED':
                this.showExpiredStatus();
                this.refreshQRCode();
                break;
        }
    }
}

配置示例

application.yml

bash 复制代码
# Redis配置
spring:
  redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD:}
    timeout: 3000ms
    database: 0

# 二维码配置
qrcode:
  expire-time: 300 # 5分钟
  polling-interval: 2000 # 轮询间隔2秒
  max-scan-attempts: 3 # 最大扫描尝试次数
  
# 安全配置
security:
  encryption:
    algorithm: AES
    key-size: 256
    iv-size: 16
  rate-limit:
    ip-max-requests: 100
    ip-window-hours: 1
    
# WebSocket配置
websocket:
  enabled: true
  endpoint: /ws
  allowed-origins: "*"
  message-size-limit: 65536

监控与日志

bash 复制代码
@Aspect
@Component
@Slf4j
public class QRCodeLogAspect {
    
    @Around("@annotation(QRCodeOperation)")
    public Object logQRCodeOperation(ProceedingJoinPoint joinPoint) throws Throwable {
        String operation = getOperationName(joinPoint);
        String qrCodeId = getQRCodeId(joinPoint.getArgs());
        
        log.info("QRCode操作开始: operation={}, qrCodeId={}", operation, qrCodeId);
        
        long startTime = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            long duration = System.currentTimeMillis() - startTime;
            
            log.info("QRCode操作成功: operation={}, qrCodeId={}, duration={}ms", 
                operation, qrCodeId, duration);
            
            // 记录监控指标
            recordMetrics(operation, duration, "success");
            
            return result;
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - startTime;
            
            log.error("QRCode操作失败: operation={}, qrCodeId={}, duration={}ms, error={}", 
                operation, qrCodeId, duration, e.getMessage());
            
            // 记录监控指标
            recordMetrics(operation, duration, "error");
            
            throw e;
        }
    }
}

注意事项

安全性考虑

二维码ID必须是不可预测的随机值

所有敏感数据传输必须加密

实施严格的访问控制和权限验证

定期更新加密密钥

性能优化

Redis使用连接池

WebSocket连接保持活跃

二维码图片使用缓存

异步处理非关键操作

容错处理

WebSocket断开时自动切换轮询

Redis故障时的降级方案

网络异常的重试机制

状态不一致的恢复策略

用户体验

二维码过期自动刷新

实时状态反馈

多设备登录提示

操作确认机制

相关推荐
guchen6615 小时前
CircularBuffer 优化历程:从数组越界到线程安全的完美实现
后端
古城小栈15 小时前
Cargo.toml
开发语言·后端·rust
brave_zhao15 小时前
launch4j亲测打包java的jar转为exe执行文件
java
利刃大大15 小时前
【RabbitMQ】SpringBoot整合RabbitMQ:工作队列 && 发布/订阅模式 && 路由模式 && 通配符模式
java·spring boot·消息队列·rabbitmq·java-rabbitmq
lkbhua莱克瓦2415 小时前
进阶-存储对象1-视图
java·数据库·sql·mysql·视图
AIGCExplore15 小时前
Jenkins 自动构建编译 Spring Boot 和 Vue 项目
vue.js·spring boot·jenkins
yangminlei15 小时前
Spring Boot 自动配置原理与自定义 Starter 开发实战
java·数据库·spring boot
悟空码字15 小时前
10分钟搞定!SpringBoot集成腾讯云短信全攻略,从配置到发送一气呵成
java·spring boot·后端
星浩AI15 小时前
从0到1:用LlamaIndex工作流构建Text-to-SQL应用完整指南
人工智能·后端·python
爱编程的小吴15 小时前
【力扣练习题】151. 反转字符串中的单词
java·算法·leetcode