打开你手机里的淘宝、微信、抖音等等 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 时,即可判定二维码已过期,前端展示“二维码已失效,请点击刷新”的遮罩。重新刷新即重新走阶段一的流程。