4.完成登录:授权通过后,后端更新Redis中二维码对应的登录状态为已登录,前端监听到登录状态变化后,完成登录流程,跳转至相应页面。
<!-- 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.1</version> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.5.1</version> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>配置
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生成二维码
/** * 堆代码 duidaima.com * 二维码生成工具类 */ 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); } }定义常量和实体类
/** * 常量类 */ 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
/** * 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; } }二维码服务实现
/** * 二维码服务 */ @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); } }后端控制器实现
/** * 登录控制器 */ @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消息处理器
/** * WebSocket消息处理器 */ @Controller public class WebSocketController { /** * 处理客户端订阅二维码状态的消息 */ @MessageMapping("/subscribeQr") @SendTo("/topic/qr/{uuid}") public WebSocketMessage subscribeQr(@PathVariable String uuid) { // 这里可以根据需要返回初始状态 return new WebSocketMessage( uuid, QRCodeStatus.WAITING, "等待扫描", null ); } }三.总结
6.前端建立WebSocket连接,准备接收状态更新(轮询方式不需要)
7.用户在移动端选择要登录的账号并确认
7.移动端显示登录成功界面