成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

SpringBoot 掃碼登錄全流程:UUID 生成、狀態輪詢、授權回調詳解

開發 前端
用戶使用手機端應用掃描二維碼,手機端應用攜帶掃碼信息請求后端,后端驗證掃碼信息并標記二維碼為已掃描狀態,同時返回授權頁面或直接進行授權操作。

前言

在移動互聯網時代,掃碼登錄以其便捷性和安全性,成為眾多應用首選的登錄方式。

技術原理

掃碼登錄的核心流程基于WebSocket、Redis等技術,主要包含以下幾個關鍵步驟:

  • 生成二維碼:用戶點擊掃碼登錄后,后端生成一個唯一的UUID作為標識,將該標識與用戶設備信息等關聯,存儲到Redis中,并生成包含此UUID的二維碼返回給前端展示。
  • 前端輪詢或WebSocket監聽:前端通過輪詢接口或使用WebSocket長連接,不斷向后端查詢二維碼對應的登錄狀態。
  • 掃碼與授權:用戶使用手機端應用掃描二維碼,手機端應用攜帶掃碼信息請求后端,后端驗證掃碼信息并標記二維碼為已掃描狀態,同時返回授權頁面或直接進行授權操作。
  • 完成登錄:授權通過后,后端更新Redis中二維碼對應的登錄狀態為已登錄,前端監聽到登錄狀態變化后,完成登錄流程,跳轉至相應頁面。

實現

演示效果

輪詢方式

圖片圖片

websocket

圖片圖片

引入依賴

<!-- 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

生成二維碼

/**
 * 二維碼生成工具類
 */
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
        );
    }
}

總結

二維碼生成階段

  1. 用戶打開Web登錄頁面
  2. 前端請求后端生成唯一的二維碼ID
  3. 后端生成二維碼ID,初始狀態為等待掃描
  4. 后端將二維碼ID及狀態存儲到Redis
  5. 后端生成包含二維碼ID的二維碼圖片并返回給前端
  6. 前端建立WebSocket連接,準備接收狀態更新(輪詢方式不需要)

掃描確認階段

  1. 用戶通過移動端App掃描二維碼,獲取二維碼ID
  2. 移動端發送掃描請求到服務端
  3. 服務端更新二維碼狀態為已掃描
  4. 服務端通過WebSocket推送狀態變更到Web端(輪詢方式不需要)
  5. Web端更新UI顯示已掃描狀態
  6. 移動端顯示用戶選擇界面
  7. 用戶在移動端選擇要登錄的賬號并確認

登錄完成階段

  1. 移動端發送確認登錄請求到服務端
  2. 服務端驗證二維碼狀態,生成用戶令牌
  3. 服務端更新二維碼狀態為已確認,并附帶用戶信息
  4. 服務端通過WebSocket推送登錄成功信息到Web端(輪詢方式不需要)
  5. Web端接收到登錄成功消息,獲取用戶信息
  6. Web端完成登錄流程,顯示用戶信息
  7. 移動端顯示登錄成功界面

功能優化與擴展

安全增強

  • 數據加密:對二維碼內容和傳輸的數據進行加密處理,防止信息泄露。
  • 防重放攻擊:為每個請求添加時間戳和簽名,防止請求被截獲后重放。
  • 訪問控制:限制對Redis數據的訪問權限,只允許授權的請求操作相關數據。
  • IP限制:對頻繁請求的IP進行限制,防止惡意攻擊。

多設備支持

  • 設備管理:記錄和管理用戶登錄的設備信息,支持查看和管理已登錄設備。
  • 異地登錄提醒:當檢測到用戶在異地登錄時,發送提醒通知。
  • 單點登錄:實現同一賬號在不同設備上的單點登錄功能。
責任編輯:武曉燕 來源: 一安未來
相關推薦

2025-06-04 04:00:00

Spring掃碼登錄免密認證

2023-11-20 08:29:33

Vue微信掃碼授權登錄

2017-08-24 11:54:43

Linux日志定時輪循機制

2022-01-13 17:24:04

SpringBootYml監聽器

2022-01-14 14:50:14

SpringBootymlJava

2020-03-08 15:39:41

微信掃碼登陸二維碼

2021-10-26 10:29:45

掃碼登錄功能

2009-08-19 16:40:35

C#回調

2014-06-18 09:25:07

HTTP

2020-04-15 16:30:24

掃碼登錄微信前端

2022-06-10 06:55:21

JustAuthSpring

2022-07-01 08:02:30

QQ掃碼登錄

2021-11-06 07:42:04

驗證開發流程

2021-01-21 09:31:21

QQ微信移動應用

2024-11-06 13:03:06

2021-09-08 10:02:56

面試二維碼前端

2024-03-07 07:59:37

2019-05-21 15:15:22

掃碼乘車公交卡支付方式

2017-12-28 10:10:15

2025-04-25 08:30:00

前端后端用戶登錄
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美黄 片免费观看 | 日韩av资源站 | 亚洲国产一区二区三区 | 久在线 | 操亚洲 | 欧美性区| 国产黄色精品在线观看 | 国产欧美在线播放 | 中文字幕av亚洲精品一部二部 | 亚洲综合视频一区 | 欧美一区二区三区四区视频 | 五月婷婷丁香 | 波多野结衣先锋影音 | 亚洲黄色av | 亚洲精品一级 | 久久亚洲一区二区三区四区 | www.99精品| 男女一区二区三区 | 一区二区国产精品 | 中文字幕在线精品 | 日韩国产精品一区二区三区 | 少妇一级淫片aaaaaaaaa | 国产精品99一区二区 | 亚洲国产区 | 国产在线一区二区三区 | 午夜精品福利视频 | 国产成人精品久久 | 国产精品日韩欧美一区二区三区 | 夜夜av| 久久99网| 亚洲国产成人精品女人久久久 | www久久久 | 超碰在线免费av | 国产日屁 | 好好的日在线视频 | 一区二区三区视频在线观看 | 天天做日日做 | 国产亚洲高清视频 | 激情一区二区三区 | 欧美高清视频 | 福利视频一区二区 |