啟君手機中之淘寶、微信、抖音等App,可見「掃碼登錄」已成幾乎所有多端(PC端 + 移動端)應用之標配功能。
於用戶而言,掃碼登錄極大提升體驗,即無需於鍵盤繁瑣輸入賬號密碼,僅需持手機輕輕一掃,即可完成授權。而於企業而言,可藉此技術將PC端流量向移動端引流、提升App日活與用戶黏性,乃重要運營手段。
然則,此看似簡單之「一掃一按」背後,究竟隱藏何等技術實現原理?前後端如何配合?
一、 為何吾等需掃碼登錄?
深入技術細節之前,先理清業務背景。傳統賬號密碼登錄存有數痛點:
- 輸入成本高:尤於公共設備或陌生設備上,輸入長串密碼甚為不便。
- 安全風險:於網吧或公共電腦輸入密碼,存有被鍵盤記錄器等木馬盜取之風險。
- 多端割裂:用戶於手機上明明已處登錄狀態,然於PC上仍需重走一遍身份驗證流程。
掃碼登錄完美解決此類問題。其本質為:利用已完成身份認證之移動端App,去授權未完成身份認證之PC端。
二、 掃碼登錄之核心生命週期
無論微信掃碼、QQ掃碼抑或各類內部系統之掃碼登錄,其底層邏輯萬變不離其宗。吾等可將整個複雜生命週期抽象為五大步驟,並於工程實現上將其精簡為三大核心階段。
完整用戶體驗五步曲:
- 生成二維碼:PC端展示一個帶有時效性之二維碼。
- 掃碼:用戶掏出手機App掃描該二維碼。
- 確認登錄:手機App提示「是否允許登錄PC端」,用戶點擊確認。
- PC端輪詢/監聽:PC端感知授權成功。
- 登錄成功: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 時,即可判定二維碼已過期,前端展示「二維碼已失效,請點擊刷新」之遮罩。重新刷新即重新走階段一之流程。