打開你手機裡的淘寶、微信、抖音等等 App,你會發現「掃碼登入」已經成為了幾乎所有多端(PC 端 + 行動端)應用的標配功能。
對於使用者而言,掃碼登入極大地提升了體驗,即無需在鍵盤上繁瑣地輸入帳號密碼,只需拿起手機輕輕一掃,即可完成授權。而對於企業而言,可以透過這一技術將 PC 端流量向行動端引流、提升 App 日活和使用者黏性的重要營運手段。
那麼,這個看似簡單的「一掃一按」背後,到底隱藏著怎樣的技術實作原理?前後端是如何配合的?
一、 為什麼我們需要掃碼登入?
在深入技術細節之前,我們先理清業務背景。傳統的帳號密碼登入存在幾個痛點:
- 輸入成本高:尤其是在公共裝置或陌生裝置上,輸入長串密碼非常不便。
- 安全風險:在網咖或公共電腦上輸入密碼,存在被鍵盤記錄器等木馬盜取的風險。
- 多端割裂:使用者在手機上明明已經處於登入狀態,但在 PC 上還要重新走一遍身份驗證流程。
掃碼登入完美解決了這些問題。它的本質是:利用已經完成身份認證的行動端 App,去授權未完成身份認證的 PC 端。
二、 掃碼登入的核心生命週期
無論是微信掃碼、QQ 掃碼還是各類內部系統的掃碼登入,其底層邏輯萬變不離其宗。我們可以將整個複雜的生命週期抽象為五個大步驟,並在工程實作上將其精簡為三大核心階段。
完整的使用者體驗五步曲:
- 生成 QR Code:PC 端展示一個帶有時效性的 QR Code。
- 掃碼:使用者掏出手機 App 掃描該 QR Code。
- 確認登入:手機 App 提示「是否允許登入 PC 端」,使用者點擊確認。
- PC 端輪詢/監聽:PC 端感知到授權成功。
- 登入成功:PC 端獲取到身份憑證(Token),正常存取系統。
在後端架構與業務流程設計上,我們將其提煉為三個核心技術步驟:生成 QR Code -> 掃碼 -> 確認登入。下面我們將逐一進行原始碼級深度的剖析。
三、 核心步驟拆解與技術實作
要實作掃碼登入,我們需要藉助一個關鍵的中介軟體:Redis。因為掃碼登入涉及到多端(PC 瀏覽器與行動端 App)之間的狀態共享與通訊,而 Redis 憑藉其高效能的記憶體讀寫和 Key 過期機制,是處理此類狀態流轉的最佳選擇。
階段一:生成 QR Code (構建授權憑證)
當使用者在 PC 端點擊「掃碼登入」時,故事就開始了。
1. 前端請求後端生成憑證
PC 端前端會向後端伺服器發起一個請求,要求獲取登入 QR Code。
2. 後端生成全域唯一 ID (QR_Code_ID)
後端接收到請求後,會生成一個全域唯一的 ID(通常使用 UUID 即可)。這個 ID 就是這個 QR Code 的「靈魂」,它將貫穿整個登入流程。
3. Redis 狀態儲存
後端拿到這個生成的 QR_Code_ID 後,會將其存入 Redis 中。在這裡,我們需要維護 QR Code 的狀態機。初始狀態為 NEW(待掃描)。
為了防止 QR Code 無限期有效導致安全漏洞和記憶體洩漏,我們必須為這個 Key 設定一個過期時間(TTL),通常是 1 到 5 分鐘。
Redis 資料結構範例:
- Key:
qrcode:status:{QR_Code_ID} - Value:
NEW(或對應的列舉值 0) - Expire:
300s
4. 回應前端並生成圖片
後端將這個 QR_Code_ID 返回給 PC 端。
關於 QR Code 圖片的生成,有兩種流派:
- 後端生成:後端透過 ZXing 等庫,將
QR_Code_ID結合特定的 URL Scheme 生成 QR Code 圖片,轉成 Base64 字串返回給前端直接渲染。 - 前端生成(推薦):後端只返回
QR_Code_ID和掃描跳轉的協議連結(如myapp://login/auth?qrcodeId=xxx),前端利用 JavaScript 庫(如qrcode.js)在本地瀏覽器直接生成圖片。這種方式可以極大地減輕伺服器的 CPU 計算壓力。
階段二:App 掃碼 (狀態流轉與臨時授權)
QR Code 生成後,PC 端螢幕上就出現了一個等待被掃描的圖案。此時,PC 端不能傻等著,它需要知道使用者到底掃沒掃。
1. PC 端的「翹首以盼」(狀態同步)
PC 端需要不斷地向後端確認這個 QR Code 的狀態。常見的技術方案有三種:
- 短輪詢 (Short Polling):前端利用
setInterval每隔 1-2 秒向後端發一次 HTTP 請求。實作極其簡單,但對伺服器壓力大,產生大量無效請求。(淘寶目前部分場景仍在使用輪詢)。 - 長連結 (WebSocket):PC 端和後端建立雙向通訊通道。狀態一旦改變,後端主動推送到前端。即時性極高,但增加了後端的架構複雜度。
- SSE (Server-Sent Events):一種單向的長連結技術,後端可以向前端推送資料。在掃碼登入這種「後端向前端單向通知」的場景中非常合適。
2. App 端發起掃碼
使用者打開手機 App,且此時手機 App 必須已經是登入狀態。App 呼叫系統相機掃描 QR Code,解析出其中的 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,頁面會立刻回饋,通常是給 QR Code 蒙上一層半透明的遮罩,並提示:「掃描成功,請在手機端確認登入」。
階段三:確認登入 (頒發正式憑證)
此時,壓力來到了手機 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 與 QR Code:為了讓 PC 端拿到這個 Token,後端需要把生成的
PC_User_Token暫時儲存在 Redis 中,Key 可以依然是這個QR_Code_ID。
3. PC 端拿到鑰匙,大門敞開
PC 端的那一次輪詢(或者 WebSocket 監聽)終於等到了好消息。它查詢到 QR Code 狀態變成了 VERIFIED,並且後端在回應中附帶了剛剛生成的 PC_User_Token。
PC 端前端拿到這個 Token 後,將其存入本地的 Cookie 或 LocalStorage 中。隨後,頁面進行重定向跳轉到系統的首頁。
至此,整個掃碼登入流程完美閉環!
四、核心程式碼結構展示
下面是一段後端處理邏輯的偽代碼骨架,展示狀態機的判斷邏輯。
列舉類:QR Code 狀態定義
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. 安全性考量:QR Code 被劫持怎麼辦?
整個通訊過程必須嚴格採用 HTTPS 協議,防止中間人抓包。此外,QR Code 的 QR_Code_ID 應當具有足夠的隨機性(比如 UUIDv4),防止被暴力遍歷或猜測。在 App 掃碼時,還可以增加裝置環境檢測(如 IP 位址異地風險提示),如果發現掃碼 IP 與 PC 端請求 IP 跨省份,可以增加二次驗證。
2. 為什麼一定要分成「已掃描」和「已確認」兩個狀態?
這是經典的 CSRF(跨站請求偽造)防禦思想。如果掃碼即登入,駭客可以將你的登入 QR Code 偽裝成一個誘導性的圖片(比如「掃碼領紅包」)發到群裡。不知情的使用者一旦用 App 掃碼,駭客的 PC 端就直接登入了你的帳號。引入「待確認」狀態,強制使用者在 App 介面上看到「確認登入 Windows 版微信」等明確提示,將最終授權的決定權交還給使用者。
3. QR Code 過期機制如何設計?
依託於 Redis 的 TTL 機制。當生成 QR_Code_ID 時,設定 Key 的過期時間為 3 分鐘。PC 端的輪詢介面發現查不到該 Key 時,即可判定 QR Code 已過期,前端展示「QR Code 已失效,請點擊重新整理」的遮罩。重新整理即重新走階段一的流程。