Java案例如何实现二维码扫码登录?

wen java案例 5

本文目录导读:

Java案例如何实现二维码扫码登录?

  1. 扫码登录基本原理
  2. 完整实现代码
  3. 关键优化建议
  4. 部署配置

我来详细介绍二维码扫码登录的实现方案,包含完整的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轮询两种模式,并考虑了安全性和性能优化。

抱歉,评论功能暂时关闭!