啟君手機中之淘寶、微信、抖音等App,可見「掃碼登錄」已成幾乎所有多端(PC端 + 移動端)應用之標配功能。

於用戶而言,掃碼登錄極大提升體驗,即無需於鍵盤繁瑣輸入賬號密碼,僅需持手機輕輕一掃,即可完成授權。而於企業而言,可藉此技術將PC端流量向移動端引流、提升App日活與用戶黏性,乃重要運營手段。

然則,此看似簡單之「一掃一按」背後,究竟隱藏何等技術實現原理?前後端如何配合?

一、 為何吾等需掃碼登錄?

深入技術細節之前,先理清業務背景。傳統賬號密碼登錄存有數痛點:

  1. 輸入成本高:尤於公共設備或陌生設備上,輸入長串密碼甚為不便。
  2. 安全風險:於網吧或公共電腦輸入密碼,存有被鍵盤記錄器等木馬盜取之風險。
  3. 多端割裂:用戶於手機上明明已處登錄狀態,然於PC上仍需重走一遍身份驗證流程。

掃碼登錄完美解決此類問題。其本質為:利用已完成身份認證之移動端App,去授權未完成身份認證之PC端。

二、 掃碼登錄之核心生命週期

無論微信掃碼、QQ掃碼抑或各類內部系統之掃碼登錄,其底層邏輯萬變不離其宗。吾等可將整個複雜生命週期抽象為五大步驟,並於工程實現上將其精簡為三大核心階段

完整用戶體驗五步曲:

  1. 生成二維碼:PC端展示一個帶有時效性之二維碼。
  2. 掃碼:用戶掏出手機App掃描該二維碼。
  3. 確認登錄:手機App提示「是否允許登錄PC端」,用戶點擊確認。
  4. PC端輪詢/監聽:PC端感知授權成功。
  5. 登錄成功:PC端獲取身份憑證(Token),正常訪問系統。

於後端架構與業務流程設計上,吾等將其提煉為三個核心技術步驟:生成二維碼 -> 掃碼 -> 確認登錄。下文將逐一進行源碼級深度剖析。


三、 核心步驟拆解與技術實現

欲實現掃碼登錄,吾等需藉助一關鍵中間件:Redis。因掃碼登錄涉及多端(PC瀏覽器與移動端App)間之狀態共享與通信,而Redis憑藉其高性能內存讀寫及Key過期機制,乃處理此類狀態流轉之最佳選擇。

階段一:生成二維碼 (構建授權憑證)

當用戶於PC端點擊「掃碼登錄」時,故事便開始了。

1. 前端請求後端生成憑證

PC端前端會向後端服務器發起一個請求,要求獲取登錄二維碼。

2. 後端生成全局唯一ID (QR_Code_ID)

後端接收請求後,會生成一個全局唯一之ID(通常使用UUID即可)。此ID即為此二維碼之「靈魂」,將貫穿整個登錄流程。

3. Redis 狀態存儲

後端取得此生成之 QR_Code_ID 後,會將其存入 Redis 中。於此處,吾等需維護二維碼之狀態機。初始狀態為 NEW(待掃描)。

為防二維碼無限期有效導致安全漏洞與內存洩漏,吾等必須為此 Key 設置一個過期時間(TTL),通常為 1 至 5 分鐘。

Redis 數據結構示例:

  • Key: qrcode:status:{QR_Code_ID}
  • Value: NEW (或對應枚舉值 0)
  • Expire: 300s

4. 響應前端並生成圖片

後端將此 QR_Code_ID 返回給 PC端。

關於二維碼圖片之生成,有兩種流派:

  • 後端生成:後端通過 ZXing 等庫,將 QR_Code_ID 結合特定 URL Scheme 生成二維碼圖片,轉成 Base64 字符串返回給前端直接渲染。
  • 前端生成(推薦):後端只返回 QR_Code_ID 及掃描跳轉之協議鏈接(如 myapp://login/auth?qrcodeId=xxx),前端利用 JavaScript 庫(如 qrcode.js)於本地瀏覽器直接生成圖片。此法可極大減輕服務器之 CPU 計算壓力。

階段二:App掃碼 (狀態流轉與臨時授權)

二維碼生成後,PC端屏幕上便出現一個等待被掃描之圖案。此時,PC端不能傻等,它需知曉用戶究竟掃沒掃。

1. PC端之「翹首以盼」(狀態同步)

PC端需不斷向後端確認此二維碼之狀態。常見技術方案有三種:

  • 短輪詢 (Short Polling):前端利用 setInterval 每隔 1-2 秒向後端發一次 HTTP 請求。實現極其簡單,然對服務器壓力大,產生大量無效請求。(淘寶目前部分場景仍在使用輪詢)。
  • 長鏈接 (WebSocket):PC端和後端建立雙向通信通道。狀態一旦改變,後端主動推送至前端。實時性極高,然增加了後端之架構複雜度。
  • SSE (Server-Sent Events):一種單向之長鏈接技術,後端可向前端推送數據。於掃碼登錄此種「後端向前端單向通知」之場景中非常合適。

2. App 端發起掃碼

用戶打開手機 App,且此時手機App必須已是登錄狀態。App調用系統攝像頭掃描二維碼,解析出其中之 QR_Code_ID

3. App 向後端發送掃碼請求

App 取得 QR_Code_ID 後,立刻向後端發送一個「我掃了」之請求。

此請求非常關鍵,必須攜帶兩樣東西:

  • 剛剛解析出之 QR_Code_ID
  • App 當前用戶之身份憑證(如 App_User_Token)。

4. 後端處理掃碼邏輯

後端收到請求後,首先校驗 App_User_Token 之合法性。若 Token 失效,會要求用戶先在 App 上登錄。

校驗通過後,後端確認了「是何用戶正在掃碼」。接著,後端會對 Redis 進行更新:

  • qrcode:status:{QR_Code_ID} 之狀態從 NEW(待掃描)修改為 SCANNED(已掃描待確認)。
  • 注意:生成臨時 Token (Temp_Token)。後端生成一個一次性之臨時 Token,並將其與 QR_Code_ID 綁定存入 Redis。然後將此 Temp_Token 返回給 App 端。

為何不直接登錄?為何要多一步「臨時 Token」?

此乃為了安全與用戶控制權。掃碼並不意味立刻授權,用戶有可能是不小心掃到,或是在不知情之情況下被誘導掃碼。因此,App 掃碼後,必須於手機上彈出一個「確認登錄 PC 端」之界面。此臨時 Token 便是為下一步「確認操作」做防偽驗證,確保確認請求確實來自剛才掃碼之同一台手機。

此時,PC端通過輪詢或 WebSocket 感知到 Redis 中狀態變成了 SCANNED,頁面會立刻反饋,通常是給二維碼蒙上一層半透明遮罩,並提示:「掃描成功,請於手機端確認登錄」。


階段三:確認登錄 (頒發正式憑證)

此時,壓力來到了手機 App 這邊,界面上正展示著「確認登錄 Web 端」之按鈕。

1. App 發送確認請求

用戶點擊「確認登錄」。App 會攜帶上一步獲取之臨時憑證 Temp_Token,向後端發起最終之確認請求。

2. 後端執行最終校驗與授權

後端收到確認請求,開始執行最核心之登錄邏輯:

  • 校驗 Temp_Token:去 Redis 中核對此臨時 Token 是否合法、是否與當前要操作之 QR_Code_ID 匹配。
  • 防重複處理:一旦校驗成功,立刻將此 Temp_Token 從 Redis 中刪除(或標記失效),保證其一次性使用之安全性。
  • 狀態變更為已確認:將 qrcode:status:{QR_Code_ID} 之狀態從 SCANNED 變更為 VERIFIED(已確認/已使用)。
  • 頒發 PC 端 Token:既然用戶已經同意,後端此時會為 PC 端生成一個全新之、具有相應權限之 PC_User_Token
  • 綁定用戶信息:將 PC_User_Token 與該用戶之賬號信息綁定,存入 Redis(或後端之 Session 體系中)。
  • 關聯 Token 與 二維碼:為讓 PC 端取得此 Token,後端需將生成之 PC_User_Token 暫時存儲於 Redis 中,Key 可依然是此 QR_Code_ID

3. PC端拿到鑰匙,大門敞開

PC 端之那次輪詢(或者 WebSocket 監聽)終於等到了好消息。它查詢到二維碼狀態變成了 VERIFIED,並且後端在響應中附帶了剛剛生成之 PC_User_Token

PC 端前端取得此 Token 後,將其存入本地之 Cookie 或 LocalStorage 中。隨後,頁面進行重定向跳轉至系統之首頁。

至此,整個掃碼登錄流程完美閉環!


四、核心代碼結構展示

下面是一段後端處理邏輯之偽代碼骨架,展示狀態機之判斷邏輯。

枚舉類:二維碼狀態定義

public enum QRCodeStateEnum {
    NEW(0, "待扫描"),
    SCANNED(1, "已扫描,待确认"),
    VERIFIED(2, "已确认登录"),
    EXPIRED(3, "二维码已过期");

    // ... 省略属性和构造方法
}

後端確認登錄接口

@PostMapping("/qrcode/confirm")
public Result confirmLogin(@RequestParam("tempToken") String tempToken,
                           @RequestHeader("App-Token") String appToken) {

    // 1. 校验 App Token 是否有效
    UserInfo user = authService.verifyAppToken(appToken);
    if (user == null) {
        return Result.error(ErrorCodeEnum.LOGIN_FAIL, "App登录已失效,请重新登录");
    }

    // 2. 校验临时 Token,并解析出对应的二维码 ID
    String qrCodeId = redisService.get(RedisKey.TEMP_TOKEN_PREFIX + tempToken);
    if (StringUtils.isBlank(qrCodeId)) {
        return Result.error(ErrorCodeEnum.EXPIRED, "操作已过期,请重新扫码");
    }

    // 3. 防并发与幂等:删除临时 Token
    redisService.delete(RedisKey.TEMP_TOKEN_PREFIX + tempToken);

    // 4. 生成 PC 端的正式 Token
    String pcToken = jwtUtils.generateToken(user.getUserId());

    // 5. 更新二维码状态为已确认,并将 PC Token 放入 Redis 供前端轮询获取
    Map<String, Object> stateData = new HashMap<>();
    stateData.put("status", QRCodeStateEnum.VERIFIED.getCode());
    stateData.put("pcToken", pcToken);

    redisService.set(
        RedisKey.QR_CODE_STATUS_PREFIX + qrCodeId,
        stateData,
        60,
        TimeUnit.SECONDS
    );

    return Result.success("登录成功");
}

PC 端輪詢接口

@GetMapping("/qrcode/status")
public Result checkStatus(@RequestParam("qrCodeId") String qrCodeId) {

    Object data = redisService.get(RedisKey.QR_CODE_STATUS_PREFIX + qrCodeId);
    if (data == null) {
        return Result.error(ErrorCodeEnum.QRCODE_EXPIRED, "二维码已失效");
    }

    // 解析 Redis 中的状态
    Map<String, Object> stateData = (Map<String, Object>) data;
    Integer status = (Integer) stateData.get("status");

    switch (status) {
        case 0: // NEW
            return Result.success(data).setMessage("请用手机扫码");
        case 1: // SCANNED
            return Result.success(data).setMessage("已扫描,请在手机端确认");
        case 2: // VERIFIED
            // 此时 data 中包含了 pcToken
            return Result.success(data).setMessage("登录成功");
        default:
            return Result.error(ErrorCodeEnum.SERVER_ERROR, "未知状态");
    }
}

五、 異常場景與安全性設計

於真實之企業應用中,可能會面對一系列「非正常場景」:

1. 安全性考量:二維碼被劫持怎麼辦?

整個通信過程必須嚴格採用 HTTPS 協議,防止中間人抓包。此外,二維碼之 QR_Code_ID 應當具有足夠之隨機性(比如 UUIDv4),防止被暴力遍歷或猜測。於 App 掃碼時,還可增加設備環境檢測(如 IP 地址異地風險提示),若發現掃碼 IP 與 PC 端請求 IP 跨省份,可增加二次驗證。

2. 為何一定要分成「已掃描」和「已確認」兩個狀態?

此乃經典之 CSRF(跨站請求偽造)防禦思想。若掃碼即登錄,黑客可將君之登錄二維碼偽裝成一個誘導性圖片(比如「掃碼領紅包」)發至群中。不知情之用戶一旦用 App 掃碼,黑客之 PC 端便直接登錄了君之賬號。引入「待確認」狀態,強制用戶於 App 界面上看到「確認登錄 Windows 版微信」等明確提示,將最終授權之決定權交還給用戶。

3. 二維碼過期機制如何設計?

依託於 Redis 之 TTL 機制。當生成 QR_Code_ID 時,設置 Key 之過期時間為 3 分鐘。PC 端之輪詢接口發現查不到該 Key 時,即可判定二維碼已過期,前端展示「二維碼已失效,請點擊刷新」之遮罩。重新刷新即重新走階段一之流程。